#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This is a form for archiving albums.
An album (in Unibas terminology) is a usually mass-produced physical medium
like an audio CD, video DVD, data CD or DVD (e.g. game CD-ROM),
paper book, vinyl LP, MC, VHS cassette.
This form provides direct or indirect access to the content and almost all
of the metadata concerning the album. This includes
the product description, packaging (single or package of several CDs/DVDs etc.),
contents (document) titles, authors/actors/speakers and other artists (TODO),
of the original and possible copies.
In this form, the user can see if there are copies, in which format,
and initiate copies directly for some formats (e.g. MP3, OGG Vorbis, etc.)
For others, external copy and compression tools are needed,
but the user can archive the resulting files with this form.
TODO:
- Export CD as series of MP3 titles
- with index.html incl. metadata, with cover images
- document.qual field, smallint, 100=same as orig, 0=nosignal only noise, <0 special values
- protocol to exchange interesting docs among peers
- Export to FreeDB, i.e. for all audio CD albums:
if we have track titles (for some albums we have only the album title):
check if the album is already known in Free DB
if not, export the album and track titles to FreeDB
- Fix the following bug:
packageId None
[1594558216L, 8, 150, 21198, 47350, 79929, 105994, 123559, 154445, 184183, 2825]
Traceback (most recent call last):
File "albumform.py", line 611, in checkAudioCd
res = self.readFreeDb()
File "albumform.py", line 577, in readFreeDb
(read_status, info) = CDDB.read(query_info['category'], query_info['disc_id'])
File "/usr/lib/pymodules/python2.6/CDDB.py", line 92, in read
header[0] = string.atoi(header[0])
File "/usr/lib/python2.6/string.py", line 403, in atoi
return _int(s, base)
ValueError: invalid literal for int() with base 10: '<!DOCTYPE'
- React reasonably on double packaging (hierarchy of packages as in Jazz Archive)
- Ask if part or whole backup should be done ONLY IF there is no backup yet
(if there is partial backup, offer to backup the other parts)
- On Set Owner, select album or package, but not series.
- Double-layer progress bar when copying tracks from a CD
- Orig=true if lender is library?
- Preserve/create id3tags; also for already archived MP3s:
add from DB data to MP3 file
- if a product in a series has several objects and user selects one, show that one (prod.1954)!
- update display after making copies etc.
- check EANs with http://www.barcodeisland.com/ (in Postgres 9: http://www.postgresql.org/docs/current/static/isn.html)
select id,length(ean), ean, checksum_ean(substr(ean,1,length(ean)-1)::numeric), length(artno), artno, descr from product where ean IS NOT NULL AND EAN!='' and length(ean)!=13 order by id;
many 11-digit "EANs" are UPC-A 11 digits, printed without checksum, for example
09020400078 which has checksum 4 giving a complete 090204000784
upcdatabase.com
From Smarty's Barcode Scanner
The standard for EANs/UPCs/barcodes now is EAN13, i.e. incl. checksum digit,
used by amazon.de and alphamusic.de and presumably most others.
From 9020400078 we get 090204000784 etc; this may most EANs could be standardized; remain only:
# select id,rt, ean, name from product where ean IS NOT NULL AND EAN!='' and length(ean)!=13 order by id;
See also: http://www.adams1.com/faq.html#upclist
http://www.ean-search.org/perl/ean-search.pl?q=0090204016839
http://www.rebuy.de/i,120197/cd/hans-zonatelli-wassermusik-suite-1-3?ga_source=belboon_rebuy&ga_medium=AFF&ga_content=csv&ga_term=Hans+Zanotelli+-+Wassermusik%2C+Suite+1-3+CD&ga_campaign=belboon_rebuy_AFF_csv_Hans+Zanotelli+-+Wassermusik%2C+Suite+1-3+CD?belboon=01af41070ab002c96e003d9f,2361846,subid=1469064719
gepir.gs1.org
http://www.adams1.com/isbn.html
To check and reformat standard numbers:
http://arthurdejong.org/python-stdnum/
Format is maybe more than mimetype:
- DVD directory, <4.7GB
- DVD directory, >4.7GB
- splittable MP3?
class Format
TODO: Represent albumList by a class, see fillFields().
TODO: Represent each step by a class, e.g. Ripper, Encoder, Tagger, ...
The different types (of e.g. encoders) then are subclasses.
TODO: Aids to handle extras in a DVD, such as "Augusta Mozarteum" in "Eine kleine Zauberflöte".
This is a video, but usually not in a one-file archivable digital form,
and implicitly part of the root document but not a version of it.
The original is represented as a video document (mimetype 112: MP)
and its archivable version is an MPG file.
The original needs a cp entry. Its versionof field is empty.
So it is displayed as a document contained in the album,
not a version of its root doc.
(Although implicitly it is of course part of the root doc.)
Suggest
- find album
- function archivePartDoc():
- let user select file
- archive it as in newDocumentFromFile
- add origdoc and origdoc-cp entries
A book with CD can be represented as a package of ot="book with CD"
containing a book (which could be scanned) and a CD (which can be
archived as one or more MP3 files).
(TODO: export such structure diagrams with connectiontree, as graphics or text)
Inlets of CDs may be handled like this as well,
or you deal with them just as documents.
In this case... (TODO) ... similar to front image?
Steps etc. described in node 7109. Summary:
Sources are: Given: file data
- CD/DVD, external source no no
- Preripped files on HD yes no
- The archive yes yes
- Archive data only (theoretical) no yes
Destination can be:
- single document
- work
- album (root doc)
- package (complete package in one document, not implemented)
- series (complete package in one document, not implemented)
Steps:
- Get basic metadata from source (e.g. FreeDB, NOT from infosource yet)
- Show album tree hierarchy
- Fetch data from infosource
- Add data from infosource table to handled entities (products, documents etc.)
- Tag document files with metadata (id3 tagging etc.)
- Schedule action
- Rip from disc (only for source = CD or DVD)
- Regroup (split/combine)
- Encode (convert)
- Store: copy/move to destination
- Calculate hash
- Delete (data and file)
Album Form proposes next step.
Some actions can also be performed in other forms, e.g. document form.
TODO: Find a standard notation for DB relationships, similar to flow diagrams,
for graphical documentation.
For editing:
several entries can be selected
at the same time, and are given container document or artist or whatever
information.
Part - whole relation doesn't go via "version", but directly.
I.e., if there are 4 tracks on a CD for the parts of Beethoven's Eroica,
then they can be selected and be made parts of a container document,
that has no copy, and orchestra, director etc. is associated with this,
and composer as well. Composer is also associated with
the composition as such (MusicXML or whatever), so that's redundant,
but doesn't matter. Often the "composition as such" is not available,
so we need this container document.
The "composition as such" is a good candidate for an
<ulink url="node3577">abstract document</ulink>.
License of DVD copy can be:
- "copy protected", if original is copy protected and you decide
that the copy's license should match the original's license
- "fair use", if the (legal, formal) copy protection does not
apply to you(r country) or you simply decide to ignore it
- "shareware" for some DVDs released as shareware (promotion)
Another decision is what to do when sharing documents (files) and data.
That can depend on mimetype as well.
To automatize repetitive tasks it is necessary the computer
makes decisions for the user; but sometimes it needs
the user's decision. Asking the user too often wastes his time.
Asking him to rarely increases the risk of wrong decisions.
If a decision is to be taken (by the program or the user),
the program first collects facts, finds out what's to decide,
which are the options, which is the most likely option.
Also it reads settings about how to decide, in which cases
to ask the user.
Based on these, it decides whether to ask the user or not.
If not, it takes the option that has been identified as the most
likely one.
If the user needs to be asked, the program first prepares
a message text (HTML) that states what is to decide and
which options are given. (See examples in filecheck.py)
One button per option is displayed. The most likely option
is marked as default. Also a checkbox "Always decide this way"
is displayed. If the user checks it, then in this series of
decisions he is not asked again.
If he clicks Cancel, the current operation is interrupted,
and if this is part of a series, the user is asked whether
to cancel altogether. If yes, the series is abandoned altogether.
If no, the next operation in the series is performed.
(This can maybe be cascaded. If the series is abandoned and
is part of a meta-series, an outer loop, the user is asked
whether to abandon the outer loop as well, and so on.)
In any case, atomic operations that can't be completed
must be rolled back.
Genre can be a notion, and rel:
Rock'n'Roll is genre of "Rock Around the Clock"
TODO: Einheitliche Namensgebung für Nummern, Indexe, Anzahlen
Nummer: number (French: numéro) z.B. episode Nr., not necessarily consecutive, there may be holes in numbering
number (French: nombre), count or quantity (German: Anzahl), how many, prop.: qt
index: from 0 or 1 to qt or qt-1
What is counted:
s: series
A series contains packages (episodes), but it may be a null series.
In that case it is only one episode/package and will not be mentioned
in the hierarchy.
p: package
A package contains albums (products with objects), but it may be a null package,
i.e. a single album. In that the package level and will not be mentioned
in the hierarchy.
a: album
An album, i.e. at least an object, must exist, even if there is no product.
i: side
w: work
d: document
Main idea: If a level has only one element, then the level is not mentioned.
There is a class for each level, and for the element of each level.
The level class object provides:
- getName(): a name (which can be derived from the element of the upper level)
- getId(), may be None if qt<=1
- getQuantity(), quantity/count
if qt=0, there is nothing to be displayed, neither in this nor in lower levels
if qt=1, this level is null but contains lower levels, only those are displayed
(except for album/object and document level, these are always displayed)
if qt>=2, this level is displayed and the lower levels
if there is an album, the qts for series and package (and album) are >=1
- getElements(), an element yielder (factory?), yields exactly qt elements
- if there is no series, a dummy 1-element series will be constructed,
same with package (and side and work?)
- a progress bar?
The level element object provides:
- a name
- an index (0 or 1 to qt)
- as members all needed variables, e.g. for series: eObjectId, eProductId
"""
__author__ = "Volker Paul"
__copyright__ = "Copyright 2011, Volker Paul"
__license__ = "GPL 2"
__maintainer__ = "Volker Paul"
__email__ = "volker.paul@v-paul.de"
__status__ = "Development"
__docformat__ = 'restructuredtext'
import logging, sys, os, os.path, copy, re, textwrap, urllib2
from copy import deepcopy
import Levenshtein
import CDDB, DiscID
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from operator import itemgetter
import pprint
from .utilities import *
from . import document, product, object, infosource, mimetype, person
from . import db, dbq, archive, settings, instancewidget, finddialog
from . import metadatareader, connectiontree, tools, progress, dvdinfo
import entityelement
from functools import reduce
importParentDirList = ["CD","LP","DVD","MC","book"]
def getEpisodeObject(episodeId, ot):
"""Return tuple (objectId, productId) from given episode document ID."""
origAlbumSql = textwrap.dedent("""
SELECT o.id, productid FROM object o, cp
WHERE cp.documentid=%s AND cp.objectid=o.id AND o.orig
""")[1:-1] % episodeId
origAlbumDicList = db.dicList(origAlbumSql)
if origAlbumDicList:
return (origAlbumDicList[0]["id"], origAlbumDicList[0]["productid"])
smtSql = textwrap.dedent("""
SELECT o.id, productid FROM object o, cp
WHERE cp.documentid=%s AND cp.file='/' AND cp.objectid=o.id AND ot~*'%s'
""")[1:-1] % (episodeId, ot) # Same Medium Type
smtDicList = db.dicList(smtSql)
if smtDicList:
return (smtDicList[0]["id"], smtDicList[0]["productid"])
return (None, None)
def getEpisodeList(seriesId, documentId=None):
"""Return list of documents (as dictionaries) that are episodes of given series,
sorted by order in series."""
if not seriesId: return []
addCrit = "AND e.id=%s" % documentId if documentId else ""
episodesSql = textwrap.dedent("""
SELECT rel.ord, e.id, e.title FROM document e, rel
WHERE relid=63832 AND rel.ent1=5 AND rel.id1=e.id AND rel.ent2=5 AND rel.id2=%s
%s
ORDER BY ord;
""")[1:-1] % (seriesId, addCrit) # SQL to get episode documents
#print "episodesSql:", episodesSql
#print "getEpisodeList", db.dicList(episodesSql)
return db.dicList(episodesSql)
def getEpisodeObjectList(seriesId, mainOt, documentId=None):
"""Return episodeList of for all episodes of given series with object ID.
An episode is represented by the triple (ord, objectId, poductId).
Episodes without object get listed in the warning string.
"""
res = []; noMediumDoc = ''
for e in getEpisodeList(seriesId, documentId):
(oid, pid) = getEpisodeObject(e["id"], mainOt)
if oid:
res.append((int(e["ord"]) or 0, oid, pid))
else:
noMediumDoc += "%s: %s\n" % (e["id"], e["title"])
if noMediumDoc:
msgbox("There are episode documents without album:\n%s" % limitLines(noMediumDoc))
return res
class Series():
"""Represents a series."""
def __init__(self, objectId, productId, seriesId, mainOt):
self.episodeList = []
if objectId and not seriesId:
rootDocId = db.getRootDocId(objectId)
seriesId = dbq.getSeriesId(rootDocId)
# print "seriesId", seriesId
self.seriesId = seriesId
if seriesId:
# seriesItem = self.regItem(None, "series", db.descr("document", seriesId), id=seriesId)
if objectId:
msg = "This is an episode of a series. Show other episodes too?"
if QMessageBox.question(None, "Unibas AlbumForm", msg,
QMessageBox.StandardButtons(QMessageBox.No | QMessageBox.Yes),
QMessageBox.No) == QMessageBox.Yes:
self.episodeList = getEpisodeObjectList(seriesId, mainOt)
else: # Show only given episode.
self.episodeList = getEpisodeObjectList(seriesId, mainOt, rootDocId)
else: # Show all episodes.
self.episodeList = getEpisodeObjectList(seriesId, mainOt)
else: # No series, episodeList has only one element.
self.episodeList = [(0, objectId, productId)]
def getId(self):
return self.seriesId
def getElements(self):
self.progress = None
if self.getQuantity() > 1:
self.progress = QProgressDialog("Preparing display of series:\n" + self.getName(),
"Cancel", 0, self.getQuantity(), None)
self.progress.setWindowTitle("Unibas AlbumForm")
self.progress.setMinimumWidth(400)
self.progress.setWindowModality(Qt.WindowModal)
self.progress.setMinimumDuration(1)
self.progress.setAutoClose(True)
for i, (c, o, p) in enumerate(self.episodeList):
if self.progress: self.progress.setValue(i)
if self.progress and self.progress.wasCanceled(): break
yield SeriesElement(c, o, p)
if self.progress: self.progress.reset()
def getQuantity(self):
return len(self.episodeList)
def getName(self):
#if not self.seriesId: return None
#return db.readSql("SELECT title FROM document WHERE id=" + self.seriesId)
return db.descrOrNone("document", self.seriesId)
class SeriesElement():
def __init__(self, cnt, objectId, productId):
self.cnt, self.objectId, self.productId = cnt, objectId, productId
class Package():
"""Represents a package which can contain one or more albums (objects).
Replaces dbq.getObjectProductList().
Internal storage: opl (object-product-list)
is a list of tuples (objectId, productId)
where the tuple with index 0 is the package (may be (None, None))
and the following tuples represent albums.
$ python unibas_cli.py -c "import unibas.albumform; pa = unibas.albumform.Package('11141'); print pa.getObject()"
<object 11141: Mozart: Salzburg Symphonies, Eine kleine Nachtmusik>
"""
def __init__(self, ge, productId=None):
"""Input may be an album (object and/or product),
GenericEntity (object|product) or package product.
At least one of objectId, productId must be not None.
However, for each product only one album is returned.
That album is objectId if it is given, otherwise an original,
if there is. The list is ordered by object.descr.
"""
self.opl = [] #(None, None), (None, None)] # package and first album
if not ge and not productId: return
if isinstance(ge, entityelement.GenericEntity):
if ge.entityType == "object":
objectId = ge.getId() #; productId = None
else:
productId = ge.getId(); objectId = None
else:
objectId = ge
#print "objectId, productId", objectId, productId
#log = logging.getLogger("getObjectProductList")
#log.debug("objectId: %s, productId: %s" % (objectId, productId))
# Decide if it is a package or album.
# - no product ID given and none in DB -> album
# - product ID given and has prods -> package
# - product ID given and has packageId -> album
if not productId:
productId = db.readSql("SELECT productid FROM object WHERE id=%s"
% objectId, 0, 1)
sqlBase = "SELECT id FROM product p WHERE p.packageid=%s"
prods = db.valueList(sqlBase % productId) if productId else None
# log.debug("prods: %s" % unicode(prods))
if not prods: # Product is not package but still may be part of one.
# if not productId: return [(0, None, None), (1, objectId, productId)]
if not productId:
self.opl = [(None, None), (objectId, None)]
return
packageId = db.readSql("SELECT packageid FROM product WHERE id=%s"
% productId, 0, 1)
if not packageId: # Definitely not a package.
if not objectId: # Get object by productId; prefer originals.
sql = "SELECT id FROM object WHERE productid=%s ORDER BY orig DESC LIMIT 1" % productId
dl = db.dicList(sql)
objectId = dl[0]["id"] if dl else None
self.opl = [(None, None), (objectId, productId)]
return
prods = db.valueList(sqlBase % packageId) # is part of package, now select package's parts
packageObjectId = db.readSql("SELECT id FROM object WHERE productid=%s" % packageId, 0, 1)
self.opl = [(packageObjectId, packageId)]
else: # productId is package
self.opl = [(objectId, productId)]
self.packageId = self.getId()
objectList = []
# Loop over package parts (products), one entry per product,
# and select (at most) one object per product,
# preferably the given objectId or an original.
for productId in prods: # loop over package parts
#sql = "SELECT id, descr FROM object WHERE productid=%s" % productId
#sql += " ORDER BY (id=%s) DESC, orig DESC;" % (objectId or 0)
##print sql
## Ordering should ensure that given objectId is first (if given),
## otherwise an original. Only dl[0], i.e. the first, is taken.
#dl = db.dicList(sql)
#if dl: objectList.append((productId, dl[0]["id"], dl[0]["descr"]))
(oid, descr) = dbq.getOrigObjectOfProduct(productId, objectId)
if oid: objectList.append((productId, oid, descr))
# Sort objectList by descr which has index 2 in the tuples.
objectList.sort(key=lambda x: x[2])
for o in objectList:
self.opl.append((o[1], o[0]))
def getId(self):
return self.getProductId(0)
def getElements(self):
self.progress = None
if self.getQuantity() > 1:
self.progress = QProgressDialog("Preparing display of package:\n" + self.getName(),
"Cancel", 0, self.getQuantity(), None)
self.progress.setWindowTitle("Unibas AlbumForm")
self.progress.setMinimumWidth(400)
self.progress.setWindowModality(Qt.WindowModal)
self.progress.setMinimumDuration(1)
self.progress.setAutoClose(True)
for i in range(1, len(self.opl)):
if self.progress: self.progress.setValue(i-1)
if self.progress and self.progress.wasCanceled(): break
yield PackageElement(i, self.opl[i][0], self.opl[i][1])
if self.progress: self.progress.reset()
def getQuantity(self):
return len(self.opl)
def getName(self):
return db.descr("product", self.getId()) if self.getId() else ''
#def getAlbumCount(self):
#return len(self.opl) - 1
def getObjectId(self, i=1):
#if len(self.opl) < i + 1: return None
#return self.opl[i][0]
return self.opl[i][0] if len(self.opl) >= i + 1 else None
def getProductId(self, i=1):
return self.opl[i][1] if len(self.opl) >= i + 1 else None
# TODO: Maybe restructure this class, decide which methods are needed.
#def getPackageId(self):
#return self.getProductId(0)
#def getPackageObjectId(self):
#return self.getObjectId(0)
#def getObject(self, i=1):
#"""Return object which may be empty (no ID),
#but still an object, not None."""
#o = object.Object(self.getObjectId(i))
#o.readFromDb()
#return o
#def getProduct(self, i=1):
#"""Return product which may be empty (no ID),
#but still a product, not None."""
#p = product.Product(self.getProductId(i))
#p.readFromDb()
#return p
#def getPackage(self):
#return self.getProduct(0)
#def getPackageObject(self):
#return self.getObject(0)
#def getOPList(self):
#"""For compatibility with getObjectProductList()."""
#return [(i, op[0], op[1]) for (i, op) in enumerate(self.opl)]
#def getObjectDic(self, i=1):
#return getEntityDic("object", self.getObjectId(i))
#def getProductDic(self, i=1):
#return getEntityDic("product", self.getProductId(i))
def getVersionMimetypeIds(self, i=1):
objectId = self.getObjectId(i)
if not i: return []
versionSql = "SELECT DISTINCT * FROM v_albumversion WHERE albumid=" + objectId
versionDocs = db.dicList(versionSql)
ids = [d["copytypeid"] for d in versionDocs]
return ids
class PackageElement():
def __init__(self, cnt, objectId, productId):
self.cnt, self.objectId, self.productId = cnt, objectId, productId
def getObjectDic(self):
return getEntityDic("object", self.objectId)
def getProduct(self):
"""Return product which may be empty (no ID),
but still a product, not None."""
p = product.Product(self.productId)
#if not p.getId(): return p
if self.productId: p.readFromDb()
return p
def getMaintype(self):
if not self.productId: return ''
p = product.Product(self.productId)
return (p.getValue("rt") or '').lower()
def getEntityDic(entityType, id):
"""If id is a valid ID and references an entityType record,
return this record as dic, else None."""
if not id: return None
if int(getId(id)) <= 0: return None
l = db.dicList("SELECT * FROM %s WHERE id=%s" % (entityType, id))
return l[0] if l else None
def getText(item, col):
if not item: return None
return unicode(item.text(col))
def getNumOr1(item, col):
if not item: return 1
text = unicode(item.text(col)).strip()
if not text: return 1
return int(text)
def traverseRecursive(item, level=0):
"""Yield item if level>0 and all children.
"""
if level>0: yield item
for i in range(item.childCount()):
for res in traverseRecursive(item.child(i), level+1): yield res
# Docs -> DocsToArchive, docsToA; DocsInArchive, docs1a
# similar access patterns for DocsToArchive and DocsInArchive
class DocsInArchive():
"""Represents the archived documents that are associated
with a given album or package.
Useful for:
- exporting
- reformatting
- reproducing the album
- checking if an album and its documents are already
(and completely) archived, see checkBarcode()
See document.AlbumDocs()
"""
def __init__(self):
pass
class DocsToArchive(): # docsToA = DocsToArchive()
"""All documents (origs and copies) of an album or package.
Album index counts from 1, and is 1 by default.
Replaces albumList.
albumList was a list of albums where
each album is a list of (orig,copy) document tuples
"""
def __init__(self):
self.orig = {} # index -> list of Document()s
self.copy = {} # index -> list of Document()s
self.maxIndex = 0 # number of albums in package
def pprint(self):
pp = pprint.PrettyPrinter()
print "------------------ orig:"
pp.pprint(self.orig)
print "------------------ copy:"
pp.pprint(self.copy)
def getOrig(self, index=1):
"""Return list of orig Document()s in given album, default 1."""
return self.orig.get(index) or []
def getCopy(self, index=1):
"""Return list of copy Document()s in given album, default 1."""
return self.copy.get(index) or []
def getAlbumCount(self):
"""Return number of albums in package."""
return maxIndex
def getDocCount(self, index):
orig = self.getOrig(index)
if not orig: return 0
return len(orig)
def setPair(self, albumIndex, pair):
"""Set orig of given album to given orig, same with copy."""
self.orig[albumIndex] = [pair[0]]
self.copy[albumIndex] = [pair[1]]
def ensureAlbumExists(self, albumIndex):
if not self.orig.get(albumIndex): self.orig[albumIndex] = []
if not self.copy.get(albumIndex): self.copy[albumIndex] = []
def addPair(self, albumIndex, pair):
"""Add given orig to origs of given album, same with copy."""
self.ensureAlbumExists(albumIndex)
self.orig[albumIndex].append(pair[0])
self.copy[albumIndex].append(pair[1])
def albumList2Docs(self, albumList):
# print "albumList", albumList
self.maxIndex = len(albumList)
for albumIndex in range(self.maxIndex):
# print "albumIndex", albumIndex
self.orig[albumIndex+1] = []
self.copy[albumIndex+1] = []
docTuples = albumList[albumIndex]
for docTupleIndex in range(len(docTuples)):
docTuple = docTuples[docTupleIndex]
self.orig[albumIndex+1].append(docTuple[0])
self.copy[albumIndex+1].append(docTuple[1])
def docList_i(self, i):
return (self.getOrig(i), self.getCopy(i))
def getAlbumDocPair(self, albumCnt, level, cnt):
"""Return tuple (orig, copy) of single documents."""
index = 0 if level == "album" else int(cnt)-1
#(orig, copy) = (self.getOrig(albumCnt)[index],
#self.getCopy(albumCnt)[index])
(orig, copy) = (lget(self.getOrig(albumCnt), index),
lget(self.getCopy(albumCnt), index))
#else:
#(orig, copy) = (self.getOrig(albumCnt)[int(cnt)-1],
#self.getCopy(albumCnt)[int(cnt)-1])
return (orig, copy)
# replace:
# docList[i] if len(docList)>=i+1 else (None,None)
# extdocs = albumList[cnt-1] if cnt<=len(albumList) else [] # cnt is 1-based
class AlbumHierarchy(QTreeWidget):
"""Widget that represents the album hierarchy tree structure.
"""
def __init__(self, parent=None, name=None):
QTreeWidget.__init__(self)
self.createContextMenu()
# TODO: Determine device if necessary.
self.device = None #settings.getSetting("Devices/dvdwriter") or None
self.lastImportedType = None # old ot, so next dir import proposal will be of this type
self.baseColumnWidths = None
#self.lastMediumType = '' # used as default ot in addPackage
self.lastPackageDataset = ('', '', 'CD', 2)
self.clear() # defines and clears some variables
# self.albumDirList): # in importDirectory(), possibly 1-element album list, set by setImportPackage()
# h.defaultFormats is a dictionary maintype=>list of formatIds;
# For the first of these formats, a copy per doc is scheduled by default if it does not exist yet.
# TODO: Make customizable in settings, but offer only supported formats (mimetype.support)
# select id, type, split_part(type,'/',1) from mimetype where support>0
self.defaultFormats = {"audio":['57','59','165','163'],
"video":['133','125','104','118','108','168','105'],
"text":['1'], "application":['130']}
self.backgroundColor = {
"series": "lightgrey",
"package": "cyan",
"album": "lightblue",
"side": "lightgreen",
"work": "yellow",
"document": "white"
}
QObject.connect(self, SIGNAL("currentItemChanged (QTreeWidgetItem *,QTreeWidgetItem *)"),
self.currentItemChanged)
QObject.connect(self, SIGNAL("itemCollapsed (QTreeWidgetItem *)"), self.itemCollapsed)
QObject.connect(self, SIGNAL("itemExpanded (QTreeWidgetItem *)"), self.itemExpanded)
def clear(self):
h = self
self.ean = None # see checkDisc
self.mediaid = None
self.dvdSize = None
self.objectId = None
self.maintype = None # "audio", "video" etc. for CD/DVD in drive
# in setColWidths(), this determines formats via self.defaultFormats
self.ot = None # object type, e.g. "CD","DVD","UnDVD","book","MC","LP"
self.albumTitle = None
self.invisibleRootItem().takeChildren()
# These two tell in which mode AlbumForm currently is;
# if self.discid: archiving a CD/DVD
# if dirToImport: importing a directory
# else: cleared or showing archived data; TODO: put these in __init__
self.discid = None # audio CD (FreeDB) or self-calculated discid for CD/DVD in drive
self.dirToImport = None # in and after importDirectory, the directory to import
# self.filesToArchive = None # dic documentId->file (or subdir) from importDirectory (obsolete?)
self.objectInDevice = None # ID of object in above device
self.formats = [] # list of supported mimetype.Mimetype()
self.formatColumn = {} # keys: mimetype IDs; values: column numbers
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.defineBaseColumns()
self.setHeaderLabels(self.baseColumnNames) #["Level","Title"])
# self.lastImportedType is NOT cleared!
def defineBaseColumns(self):
c=0; self.levelColumn = c # indicates level (package, album, track, ...)
c+=1; self.curColumn = c # marks current album of a package
c+=1; self.rnColumn = c # running number per parent, e.g. doc per work
c+=1; self.dnColumn = c # document counter per album object
c+=1; self.dbTitleColumn = c # title from DB
c+=1; self.extTitleColumn = c # title from external infosource
c+=1; self.idColumn = c # id (of product, document etc.)
c+=1; self.fileColumn = c # filename (without directory)
c+=1; self.albumColumn = c # tells number of albums, only in album item
c+=1; self.formatColumnStart = c # first copy format column
self.baseColumnCount = self.columnCount = c # For formats, no columns are reserved yet
self.baseColumnNames = ["Level","?","RN","DN","Title from DB","External title","ID","File","Alb"]
if not self.baseColumnWidths:
self.baseColumnWidths = [100,10,30,25,240,100,50,40,30]
def setDevice(self):
"""Set and return the CD/DVD device, in self.device.
If self.device is already set, do nothing.
Otherwise list all devices. If there are several different ones,
let the user select.
"""
if self.device: return self.device
l = []
for cddvd in ("cd","dvd"):
for rw in ("reader","writer"):
l.append(settings.getSetting("Devices/%s%s" % (cddvd, rw)))
s = set(l)
if len(s) == 1:
self.device = l[0]
return self.device
s = StringSelector(list(s))
if not s.exec_():
return None
self.device = s.selection()[0]
return self.device
def currentItemChanged(self):
item = self.currentItem()
docId = self.getDocId(item)
self.emit(SIGNAL("changedDocId"), docId)
objectId = self.getObjectId(self.currentItem())
self.emit(SIGNAL("changedObjectId"), objectId)
#TODO: also show package even if there is no package object
packageId = self.getPackageId(self.currentItem())
print("packageId ", packageId)
if packageId and not objectId:
self.emit(SIGNAL("changedProductId"), packageId)
def itemCollapsed(self):
pass #print "itemCollapsed"
def itemExpanded(self):
item = self.currentItem()
if item: item.setExpanded(True)
def setExpanded(self, b):
"""Set whether item is expanded,
and if it is read from table if necessary.
"""
QTreeWidgetItem.setExpanded(self,b)
# class AlbumHierarchy context menu and its functions
def createContextMenu(self):
"""Create tree context menu."""
self.contextMenu = QMenu()
# addMenuAction(self, self.contextMenu, "&Test", self.test)
addMenuAction(self, self.contextMenu, "Show &info", self.showInfo)
addMenuAction(self, self.contextMenu, "&Show archived version", self.showVersion)
addMenuAction(self, self.contextMenu, "Show external version", self.showExternalVersion)
addMenuAction(self, self.contextMenu, "Show file", self.showFile)
# addMenuAction(self, self.contextMenu, "Select action to schedule", self.scheduleAction)
addMenuAction(self, self.contextMenu, "Show form", self.showForm)
addMenuAction(self, self.contextMenu, "Show Connection Tree", self.showConnectionTree)
def contextMenuEvent(self, e):
self.contextMenu.exec_(QCursor.pos())
def test(self):
pass
#h = self
##print "discid from DB:", self.productWidget.getValue("discid")
#item = self.currentItem()
#level = getText(item, h.levelColumn)
#id = getText(item, h.idColumn)
#file = getText(item, h.fileColumn)
#title = getText(item, h.dbTitleColumn)
#print(level, id, file, title)
#entityName="document"
#if level == "package": entityName="product"
#if level == "album": entityName="object"
#(e,l) = document.docParts(id, entityName)
#if e:
#errmsg(e)
#return
#msgbox(document.getStringEntityList(l))
#return
#item = item.parent()
#print("parent data:")
#level = getText(item, h.levelColumn)
#id = getText(item, h.idColumn)
#file = getText(item, h.fileColumn)
#title = getText(item, h.dbTitleColumn)
#print(level, id, file, title)
#for item in traverseRecursive(self.invisibleRootItem()):
#print(unicode(item.text(h.dbTitleColumn)), ("SELECTED" if item.isSelected() else ''))
def showInfo(self):
"""Show detailed info about current node and relatives,
from KnowledgeTree and node table.
"""
pass
#item = self.currentItem()
#level = getText(item, self.levelColumn)
#id = getText(item, self.idColumn)
#file = getText(item, self.fileColumn)
#title = getText(item, self.dbTitleColumn)
#print(level, id, file, title)
#(e,l) = document.docParts(id, level, title)
#if e:
#errmsg(e)
#return
#s = "<pre>%s</pre>" % document.getStringEntityList(l)
#print(s)
#msgbox(s)
#return
#h = self
#item = self.currentItem()
#albumId = self.getAncestorAlbumId(item)
#msgbox("albumId:", albumId)
#return
#level = getText(item, self.levelColumn)
#id = getText(item, self.idColumn)
#file = getText(item, self.fileColumn)
#title = getText(item, self.dbTitleColumn)
## msg = "level: %s, id: %s, file: %s, title: %s" % (level, id, file, title)
#msg = getItemDescr(h, item) + '\n'
#descs = getDescendantItems(h, item)
#for i in descs:
#msg += getItemDescr(h, i) + '\n'
#msgbox(msg)
def showVersion(self):
"""Show archivable version of item's document."""
docId = self.getDocId(self.currentItem())
if not docId:
errmsg("Cannot show document, no document ID!")
return
# document.showDocumentVersion(docId)
document.showDocumentVersionDialog(docId)
def showExternalVersion(self):
"""Show/playback external version, i.e. from CD/DVD or
from the directory to import."""
item = self.currentItem()
#print "self.ot:", self.ot
#print "self.device:", self.device
#print "self.maintype ", self.maintype
cnt = getText(item, self.rnColumn)
level = getText(item, self.levelColumn)
if self.discid:
ot = otHead(self.ot)
device = settings.getSetting("Devices/%sreader" % ot)
if ot == 'CD' and self.maintype == "audio":
# xine cdda:/[<device>][/<track-number>]
if level == "album": cnt = ''
# cmd = "gxine cdda://" + cnt # TODO: make configurable, and ensure it is available
cmd = "gxine cdda:/{}/{}".format(device, cnt) # for MRL syntax see man 5 xine
# cmd = "mplayer cdda://%s &" % cnt # TODO: make configurable, and ensure it is available
btc(cmd)
return
elif ot == 'DVD':
if level != "album":
msgbox("Only whole DVD can be played")
cmd = "xine dvd:/" # TODO: make configurable, and ensure it is available
btc(cmd)
return
else: #if self.ot == 'CD' and self.maintype == "audio":
# Try to mount; TODO!
#bt("mount /media/cdrom")
#bt("konqueror /media/cdrom &")
bt("mount '%s'" % device)
bt("konqueror '%s' &" % device)
elif self.dirToImport:
if level != "document": # or level == "album":
msgbox("Currently only for documents!")
elif level == "document":
albumItem = self.getAncestorAlbumItem(item)
albumCnt = int(getText(albumItem, self.rnColumn))
docDic = self.albumList[albumCnt-1][int(cnt)-1] # TODO
srcPath = docDic["albumDir"] + '/' + docDic["file"]
btc("vlc '%s'" % srcPath)
else:
msgbox("No CD/DVD and no directory to import")
def showFile(self):
"""TODO"""
file = getText(self.currentItem(), self.fileColumn)
ext = file.split('.')[-1]
m = db.dicList("SELECT * FROM mimetype WHERE ext=%s" % q(ext))
if not m:
errmsg("Can't guess mimetype")
return
dir = getText(self.currentItem().parent(), self.fileColumn)
cmd = "%s '%s/%s' &" % (m[0]["program"], dir, file)
os.system(cmd.encode('utf-8'))
# some AlbumHierarchy helper functions
def getRootDocId(self, item):
"""Return root document ID, i.e. whose cp.file='/', of an album item. May be None."""
#objectId = getText(item, self.idColumn)
id = getText(item, self.idColumn)
level = getText(item, self.levelColumn)
if level == "package":
objectId = dbq.getObjectOfSimpleProduct(id)
else:
objectId = id
print("objectId", objectId)
d = db.getRootDocDic(objectId)
return d["id"] if d else None
def getDocId(self, item):
"""Return document ID of current item.
For series, work and document levels, this is the item's id itself."""
id = getText(item, self.idColumn)
level = getText(item, self.levelColumn)
if level == "package": return getProductRootDocId(id)
if level == "album": # For album items, object ID is stored in id field.
return self.getRootDocId(item)
return id
def getObjectId(self, item):
"""Return object ID of item. For all levels except album and package, return None."""
id = getText(item, self.idColumn)
level = getText(item, self.levelColumn)
if level == "album": return id
if level == "package": return db.getMostCompleteAlbum(id)[0]
return None
def getPackageId(self, item):
"""Return package (product) ID of item if level is package, else None."""
if getText(item, self.levelColumn) == "package":
return getText(item, self.idColumn)
return None
def showForm(self):
"""Show Unibas document form of current item."""
item = self.currentItem()
level = getText(item, self.levelColumn)
id = getText(item, self.idColumn)
if not id:
errmsg("No document")
return
# entityDic = {"package":"product", "album":"product", "work":"document", "document":"document"}
# if not level in entityDic:
# errmsg("Unknown entity for level " + level)
# return
# instancewidget.showEntityForm(self, entityDic[level], id)
instancewidget.showEntityForm(self, "document", id)
def showConnectionTree(self):
"""Show Unibas connction tree of current item."""
item = self.currentItem() or self.getAlbumItem()
if not item: return
level = getText(item, self.levelColumn)
id = self.getDocId(item)
if id:
connectiontree.ConnectionTreeForm(self, "document", id, 'id').show()
return
id = getText(item, self.idColumn)
# if not id: id = self.productWidget.getId()
# if not id: id = self.objectWidget.getId()
if not id:
errmsg("Could not find out ID")
return
entityDic = {"package":"product", "album":"object", "work":"document", "document":"document"}
if not level in entityDic:
errmsg("Unknown entity for level " + level)
return
# instancewidget.showEntityForm(self, entityDic[level], id)
connectiontree.ConnectionTreeForm(self, entityDic[level], id, 'id').show()
def getAncestorAlbumId(self, item):
"""Return ID of given item if it is an album or of
an ancestor if that is an album.
"""
while 1:
if getText(item, self.levelColumn) == "album":
return getText(item, self.idColumn)
item = item.parent()
if not item: return None
def getAncestorAlbumItem(self, item):
"""Return given item if it is an album or
the ancestor that is an album.
"""
while 1:
if getText(item, self.levelColumn) in ("album", "package"): return item
item = item.parent()
if not item: return None
def getProductWithEan(self, item):
"""Return next product with EAN in hierarchy (as dictionary), or None.
"""
while 1:
id = getText(item, self.idColumn)
level = getText(item, self.levelColumn)
if level == "album":
productId = db.readSql("SELECT productid FROM object WHERE id=%s" % id, 0, 1)
if productId:
dl = db.dicList("SELECT * FROM product WHERE id=%s" % productId)
if dl and dl[0]["ean"]: return dl[0]
elif level == "package":
dl = db.dicList("SELECT * FROM product WHERE id=%s" % id)
if dl and dl[0]["ean"]: return dl[0]
item = item.parent()
if not item: return None
def checkBarcode(self):
"""
TODO: first part reused in setProduct()
TODO: Develop Unibas barcode system which codes entity and id
print such barcodes on objects and in rooms
also encode commands such as "speak all objects in this room"
barcode as mediaid -> object -> product
or barcode as ean -> product
check if product has EAN or artno, warn if not
product -> object -> document (root or first) (choose object with most documents)
warn if no object
warn if no document
try to show document
TODO: Handle packages, works, ...
"""
msg = ""
caption = "Check Barcode"
while 1:
msg += "Please enter barcode (EAN or library medium ID) or Object ID"
(barcode, ok) = QInputDialog.getText(None, caption, msg, QLineEdit.Normal)
if not ok: return
msg = ""
barcode = unicode(barcode).strip()
if not barcode:
if not productId: return
print "productId", productId
# self.selectProduct(productId)
print "self.fillFields(productId=%s)" % productId
self.fillFields(productId=productId)
return
objectId = None
if barcode[0] == 'o': # object ID
objectId = barcode[1:]
msg += "Object ID directly given as %s\n" % objectId
else:
res = db.lookup("v_objectowner", {"mediaid":barcode},
["id", "descr"], "%(id)s: %(descr)s")
if res:
msg += "Found via mediaid object %(id)s: %(descr)s\n" % res
objectId = res["id"]
productId = None
if objectId:
productId = db.readSql("SELECT productid FROM object WHERE id=%s" % objectId, 1, 1)
else:
ean13 = "{0:0>13}".format(barcode) # pad with leading zeroes
#print "ean13:", ean13
#product = db.lookup("product", {"ean":ean13}, ["id", "name"])
sql = "SELECT id, name FROM product WHERE lpad(ean, 13, '0')=%s" % q(ean13)
#print sql
dl = db.dicList(sql)
# product = dl[0] if dl else None
#print "product ", product
# productId = product["id"] if product else None
if dl:
productId = dl[0]["id"]
msg += "Found via EAN product %(id)s: %(name)s\n" % dl[0]
if not productId:
msg += "No product found\n"
else:
msg += "Product %s\n" % db.descrWithId("product", productId)
objectId = dbq.getObjectOfSimpleProduct(productId)
if not objectId:
msg += "Product found, but no object for it"
# for packages see docParts
#(e, d) = document.getObjectDocOrList(objectId) #,0)
# ge = entityelement.GenericEntity("object", objectId)
ge = entityelement.GenericEntity("product", productId)
albumdocs = document.AlbumDocs(ge)
(e, d) = albumdocs.getObjectDocOrList()
msg += limitLines(d) + '\n' + limitLines(e)
print "(e='%s', d='%s')" % (e, d)
#if e:
#msg += e
#else:
#if isinstance(d, list): d = d[0]
#(documentId, path) = document.getDocumentVersion(d)
#msg += "version documentId: %s; path: %s\n" % (documentId, path)
msg += 80*'_' + "\n\n"
# AlbumHierarchy checkDisc() and related functions
def checkDisc(self):
"""Determine type of disc and then use appropriate specialized function, e.g. checkAudioCd().
Also gather some information such as volumen label, publisher, publishing date.
This also pre-sets self.maintype which may be re-set later,
important for format columns.
TODO: dvd+rw-mediainfo /dev/dvd tells if it is a DVD-ROM etc.
It can tell if it's prerecorded (DVD-ROM) or DVD-R etc.,
if it's a DVD or CD (takes a while, says ":-( non-DVD media mounted, exiting..."
http://wiki.dvdlookup.org/index.php?title=Disc_Identification (but dvdlookup.org seems to be dead)
lsdvd -x /dev/dvd
http://ivs.cs.uni-magdeburg.de/bs/lehre/wise0001/bs2/iso/docs/iso.html#PVD
"""
log = logging.getLogger("checkDisc")
if not self.setDevice(): return
btc("eject -t " + self.device)
caption = "Checking disc in " + self.device
self.clear()
(self.ean, ok) = QInputDialog.getText(self, caption, "Please enter EAN (or leave blank)", QLineEdit.Normal)
if not ok: return
msg = "Please enter mediaid (or leave blank)"
(self.mediaid, ok) = QInputDialog.getText(self, caption, msg, QLineEdit.Normal)
if not ok: return
headerDic = getHeaderDic(self.device)
cdDiscidList = btc("cd-discid " + self.device).split(" ")
log.info("cdDiscidList=%s" % cdDiscidList)
audioCdText = ""
if len(cdDiscidList)>4:
audioCdText = "It has several tracks so it could be an <b>audio CD</b>.<br>"
(videoDvdLabel, videoDvdText) = testForVideoDvd(self.device)
# dvdbackup -I => DVD
# no DVD but CDNAME => CD-ROM
# cd-discid /dev/dvd | wc -w >=6 => audio CD
label = cutoffZero(headerDic["CDNAME"])
if label: labelText = "It has the volume label: <b>%s</b>.<br>" % label
else: labelText = "It has no volume label.<br>"
msg = "<p>Please tell the <b>type of the disc</b> in the drive %s.<br>" % self.device
msg += labelText + audioCdText + videoDvdText
msg += "</p>"
msgBox = QMessageBox()
msgBox.setText(msg)
msgBox.setWindowTitle(caption)
audioCdButton = msgBox.addButton("Audio CD", QMessageBox.ActionRole)
videoDvdButton = msgBox.addButton("Video DVD", QMessageBox.ActionRole)
cdRomButton = msgBox.addButton("CD-ROM", QMessageBox.ActionRole)
uncdRomButton = msgBox.addButton("UnCD-ROM", QMessageBox.ActionRole)
dvdRomButton = msgBox.addButton("DVD-ROM", QMessageBox.ActionRole)
abortButton = msgBox.addButton(QMessageBox.Abort)
if videoDvdLabel: msgBox.setDefaultButton(videoDvdButton)
elif audioCdText: msgBox.setDefaultButton(audioCdButton)
else: msgBox.setDefaultButton(cdRomButton)
msgBox.exec_()
if msgBox.clickedButton() == abortButton: return
if msgBox.clickedButton() == audioCdButton:
self.checkAudioCd()
elif msgBox.clickedButton() == videoDvdButton:
self.checkVideoDvd()
elif msgBox.clickedButton() == cdRomButton:
self.checkCdRom(label, "CD")
elif msgBox.clickedButton() == uncdRomButton:
self.checkCdRom(label, "UnCD-ROM")
elif msgBox.clickedButton() == dvdRomButton:
self.checkCdRom(label, "DVD")
# elif msgBox.clickedButton() == cdRomButton or msgBox.clickedButton() == dvdRomButton:
#elif msgBox.clickedButton() in (cdRomButton, uncdRomButton, dvdRomButton):
#self.checkCdRom(label)
def readFreeDb(self):
"""Return tuple (tracks, albumTitle, trackTitleList) or None.
Album may be unknown, in this case generic titles are returned.
"""
log = logging.getLogger("readFreeDb")
if not self.setDevice(): return None
caption = "Read Audio CD"
archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
# Open CD.
try:
cdrom = DiscID.open(self.device)
disc_id = DiscID.disc_id(cdrom)
except Exception as text:
msg = "Error opening or reading from device %s:\n%s" % (self.device, text)
log.error(msg)
errmsg(msg, caption)
return None
tracks = disc_id[1]
log.info("disc_id: %s" % disc_id)
#x = disc_id[0]
self.discid = "%x" % disc_id[0]
cdtitle = "CD"
offlineSetting = settings.getSetting("misc/offline").lower()
offline = (offlineSetting and offlineSetting[0] == 'y')
if offline:
log.warning("Offline mode, no Internet connection")
msgbox("Offline mode, no Internet connection", caption)
query_status = 200 # No error, just offline.
else:
try:
print(disc_id)
(query_status, query_info) = CDDB.query(disc_id)
except IOError:
query_status = None; error = "IOError, maybe no network connection"
# query_info contains unique pair disc_id, category in case of success
# query_status will be one of the following:
# * 200: Success
# * 211: Multiple inexact matches were found
# * 210: Multiple exact matches were found
# * 202: No match found
# * 403: Error; database entry is corrupt
# * 409: Error; no handshake. (client-side error?)
if query_status == 202: error = "No match found"
elif query_status == 403: error = "Database entry is corrupt"
elif query_status == 409: error = "No handshake (client-side error?)"
elif query_status in (200,210,211): error = None
else: error = "Unknown error"
if error:
msg = "CDDB query failed: %s\n" % error
log.error(msg)
errmsg(msg, caption)
if error or offline:
albumTitle = "(Unknown album)"
trackTitleList = []
for i in range(tracks):
trackTitleList.append("track%02d.cdda" % (i+1))
return (tracks, albumTitle, trackTitleList)
if query_status != 200: # Multiple matches, let user select.
# info['category']: The category the audio CD belongs in
# info['disc_id']: The CDDB checksum disc ID of the given disc
# info['title']: The title of the audio CD
stringList = ["%d: %s (%s)" % (i+1, e["title"], e["category"]) for (i,e) in enumerate(query_info)]
s = StringSelector(stringList)
if not s.exec_(): return
res = s.selection()
if not res: return (tracks, '', ["track%02d.cdda" % (i0+1)for i0 in range(tracks)])
i = int(getId(res[0]))-1 # i is the zero-based index from user's selection
query_info = query_info[i]
#cdtitle = query_info["title"]
# Now we have a query_info to read the detail info with album and track titles etc.
cdtitle = query_info['title']
# print "query_info",query_info
(read_status, info) = CDDB.read(query_info['category'], query_info['disc_id'])
# If successful, info contains album and track titles etc.
if read_status==210: error = None
elif read_status==401: error = "Specified entry not found"
elif read_status==402: error = "Server error"
elif read_status==403: error = "Database entry is corrupt"
elif read_status==409: error = "No handshake"
elif read_status==417: error = "Access limit exceeded"
else: error = "Unknown error"
if error:
msg = "No track info due to error: " + error
log.error(msg)
errmsg(msg, caption)
albumTitle = "(Unknown album)"
trackTitleList = []
for i in range(tracks):
trackTitleList.append("track%02d.cdda" % i+1)
return (tracks, albumTitle, trackTitleList)
# Now we have info with album and track titles etc.
self.discid = info['DISCID']
self.maintype = "audio"
charset = 'latin1'
albumTitle = info['DTITLE'].decode(charset)
extTitleList = []
for i0 in range(tracks):
title = info['TTITLE' + repr(i0)].decode(charset).replace('_',' ')
extTitleList.append(title)
return (tracks, albumTitle, extTitleList)
def checkAudioCd(self):
"""Read CDDB (FreeDB) info about audio CD in drive."""
ean, mediaid = self.ean, self.mediaid # TODO: better concept
self.clear()
self.ean, self.mediaid = ean, mediaid
self.maintype = "audio"
rt = self.maintype.title()
res = self.readFreeDb()
if not res: return
(tracks, albumTitle, origDocTitleList) = res
print ds(locals(), "tracks, albumTitle, origDocTitleList")
self.ot = "CD" # TODO: Try to read type (CD, CD-R, CD-RW...)
# rt is product.rt, i.e. "Audio", "Video", "Application" etc., from self.maintype.title()
# albumTitle is title, usually indentical with product.name and document.title
# origDocTitleList is, for albums with several documents, a Python list of document titles, e.g. track titles
# volumelabel is volume label of DVD or CD-ROM
# self.showDiscMetadata(rt, albumTitle, origDocTitleList, mt="113")
isrcDic = {}
# ean = self.ean
# TODO: Check when ISRC is useful and call icedax conditionally.
#res = bt("icedax -J -H -D /dev/dvd 2>&1") # provides ISRCs
#print "res:", res
#print "res.split('\n')", res.split('\n')
#isrcList = [e.split('\r')[2] for e in res.split('\n') if len(e)>40 and e.startswith('\rscanning for ISRCs')]
#print "isrcList ", isrcList
#try:
#isrcDic = dict([(int(e[3:5]),e[11:].strip().replace('-','')) for e in isrcList])
#except ValueError:
#isrcDic = None # no ISRC data
##if len(e)>20 and e[:2]=='T:'])
#print " ! ! ! ! ! !"
#print "isrcDic ", isrcDic
#d = textToDic(res)
#print d
#ean = d["Media catalog number"] if "Media catalog number" in d else None
# self.showDiscMetadata(rt, albumTitle, origDocTitleList, ean=ean, isrcDic=isrcDic)
self.showDiscMetadata(rt, albumTitle, origDocTitleList, ean=ean, mt="113", isrcDic=isrcDic)
def addVideoDvd(self):
"""Add generic video DVD.
"""
volumelabel = None
UnDVD = False
warnings = ''
title = "(Unknown DVD)"
self.maintype = "video"
albumTitle = title
rt = self.maintype.title()
origDocTitleList = []
self.ot = "DVD"
self.showDiscMetadata(rt, albumTitle, origDocTitleList, volumelabel=volumelabel, mt="122")
def checkVideoDvd(self):
"""Analyze DVD.
TODO: dvd+rw-mediainfo /dev/dvd tells if it is a DVD-ROM etc.
It can tell if it's prerecorded (DVD-ROM) or DVD-R etc.,
if it's a DVD or CD (takes a while, says ":-( non-DVD media mounted, exiting..."
http://wiki.dvdlookup.org/index.php?title=Disc_Identification
lsdvd -x /dev/dvd
"""
if not self.setDevice(): return
discid = self.getDiscId()
self.discid = discid
device = self.device
metainfo = btc("dvdbackup -I -i " + device + " 2>&1")
# Note: dvdbackup output of volumelabel has changed
# from original "WORD_WORD" to "Word Word", that is,
# dvdbackup does not show the real, original volumelabel.
# Dvdbackup and other tools more or less try to guess
# the DVD title from the volumelabel, but that can't work
# because the volumelabel is subject to certain
# restrictions (length, character set). Here is dvdbackup output:
# dvdbackup -I -i /dev/sr2 2>&1 | grep "DVD with title"
# Natm 2 De Dvd3
# HandBrakeCLI and blkid seem to only replace underscores with spaces:
# HandBrakeCLI -i /dev/sr2 -t 0 2>&1 | grep "DVD Title:"
# NATM 2 DE DVD3
# blkid /dev/sr2
# /dev/sr2: LABEL="NATM 2 DE DVD3" TYPE="udf"
# lsdvd and volname perform no conversion:
# lsdvd /dev/sr2 | grep "Disc Title"
# NATM_2_DE_DVD3
# volname /dev/sr2
# NATM_2_DE_DVD3
# If you want to be sure to get only the raw volumelabel, use dd:
# dd if=/dev/sr2 bs=1 skip=32808 count=32
# NATM_2_DE_DVD3
volumelabel = unicode(btc("dd if=/dev/sr2 bs=1 skip=32808 count=32")).strip()
# Check for UnDVD
self.dvdSize = getDvdSize(device)
UnDVD = False
if self.dvdSize>9123456789: # TODO: Exact max. DVD capacity?
msgbox("Size exceeds physical DVD capacity, maybe UnDVD!")
UnDVD = True
warnings = ''
for l in metainfo.splitlines(True):
if re.match("BUP and IFO",l):
warnings += l
if warnings!='':
msgbox(warnings + "\n\nProbably UnDVD")
UnDVD = True
if UnDVD:
title = "(Unknown UnDVD)"
else:
# get title proposal
if volumelabel and volumelabel != "DVDVOLUME":
title = volumelabel.replace('_',' ').title().strip()
# msgbox("Title proposal:\n" + title)
else:
title = "(Unknown DVD)"
volumelabel = None
self.maintype = "video"
albumTitle = title
# self.albumItem = self.regItem(None, "album", albumTitle)
# self.albumItem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
# self.albumItem.setExpanded(True)
# self.setupFormatColumns([])
# item = self.regItem(self.albumItem, "document", title, cnt=1)
# item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
# for i in range(len(self.baseColumnWidths)): self.setColumnWidth(i,self.baseColumnWidths[i])
rt = self.maintype.title()
origDocTitleList = []
self.ot = "UnDVD" if UnDVD else "DVD"
# rt is product.rt, i.e. "Audio", "Video", "Application" etc., from self.maintype.title()
# albumTitle is title, usually indentical with product.name and document.title
# origDocTitleList is, for albums with several documents, a Python list of document titles, e.g. track titles
# volumelabel is volume label of DVD or CD-ROM
self.showDiscMetadata(rt, albumTitle, origDocTitleList, volumelabel=volumelabel, mt="122")
def prepareAlbum(self):
"""Prepare album directory before archiving.
For example, provide MP3 files with id3v2 tags.
Or provide a DVD directory with a readme file containing metadata
like summary, actors list etc.
TODO:
in data: actors, composers etc.
format data as HTML here?
get product image:
sqlBase = "SELECT id1 FROM rel WHERE ent2=23 AND id2=%s AND relid=" % id
frontImages = db.valueList(sqlBase + "63814") or db.valueList(sqlBase + "53225") # Python list of document IDs
imagepath = document.getAvailableFile(','.join(frontImages)) if frontImages else None
How to prepare opening:
- take background image
- producer logo or
- DVD cover, or
- licence (shareware)
- put data on it:
- title
- year
- producer
- licence text
- reformat it to suit main video
"""
albumItem = self.getAlbumItem()
if not albumItem:
errmsg("Please select album")
return
objectId = getText(albumItem, self.idColumn)
productId = db.readSql("SELECT productId FROM object WHERE id=%s" % objectId, 0, 1)
if not productId:
errmsg("Please select album with product")
return
origDocId = self.getDocId(albumItem)
p = db.idDic("product", productId)
title = p["name"]
data = {"ean":p["ean"], "title":title}
if albumItem:
documentId = self.getDocId(albumItem)
print("documentId", documentId)
if documentId:
doc = db.idDic("document", documentId)
print(doc)
data["review"] = doc["review"]
data["actors"] = document.getArtists(documentId, "3")
try:
import preparedvd
except:
from . import preparedvd_simple as preparedvd
# prepare(dirpath, title, imagepath, dic):
dir = self.dirToImport or archive.selectDir() # TODO: replace self.copydir
if not dir: return
imagePath = product.getProductImageFile(productId)
if dir: preparedvd.prepare(dir, title, imagePath, data)
def checkCdRom(self, label, ot):
"""Analyze CD-ROM (game etc.).
"""
log = logging.getLogger("checkCdRom")
log.debug("label=%s" % label)
if not self.setDevice(): return
discid = self.getDiscId()
log.info("discid=%s" % discid)
self.discid = discid
title = label.title() if label else '' # TODO: replace '_' with ' '
self.maintype = "application"
rt = self.maintype.title()
origDocTitleList = [title] # TODO: This is th root doc
self.ot = ot #"CD"
self.showDiscMetadata(rt, title, origDocTitleList, mt="111")
def getDiscId(self):
"""Return calculated discid of a disc with filesystem.
Problem: User can mount only what is listed in /etc/fstab.
TODO: correct setup in fstab and settings
TODO: check algorithm, lots of DVDs and CDs have this discid:
d41d8cd98f00b204e9800998ecf8427e
d41d8cd98f00b204e9800998ecf8427e
"""
if not self.setDevice(): return
cmd = "mount %s" % self.device
# print cmd
res = btc(cmd)
# see getMountPoint(device)
realDevicePath = os.path.realpath(self.device)
cmd = "cat /etc/mtab | egrep '^%s' | cut -d' ' -f2" % realDevicePath
# print(cmd)
path = btec(cmd)
# print("getDiscId: path=", path)
res = getDirDiscId(path)
return res
cmd = "umount '%s'" % path
# print cmd
btec(cmd) # TODO: 1. more pythonic; 2. handle errors, e.g. if unmount is not possible
return res
def createIsoFile(self):
"""To copy a CD-ROM, create an ISO file with dd.
TODO: Check those options and if they really work in every case.
I had a case where the directory entries were there but the files were unreadable.
TODO: Check if it also (and better?) works with mkisofs:
mkisofs -r -o /path_to.iso /dev/cdrom
# TODO: Create filelist entry at the same time.
"""
if not self.setDevice(): return
caption = "createIsoFile"
msg = "Enter title"
#device = "/dev/cdrom"
# Tip with isoinfo came from http://www.troubleshooters.com/linux/coasterless.htm
# isoinfoText = btc("isoinfo -d -i " + self.device)
isoinfoText = btc("isoinfo -d -i " + self.device)
isodata = readDicFromText(isoinfoText)
title = readVal(isodata, 'Volume id') or readVal(isodata, 'System id') or ''
# title = ''
# if 'Volume id' in isodata: title = isodata['Volume id']
# elif 'System id' in isodata: title = isodata['System id']
title = title.title()
(title, ok) = QInputDialog.getText(None, caption, msg, QLineEdit.Normal, title)
if not ok: return
volsize = readVal(isodata, 'Volume size is')
blocksize = readVal(isodata, 'Logical block size is')
options = "bs=%s count=%s" % (blocksize, volsize) if volsize and blocksize else 'bs=2048 conv=notrunc'
archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
path = uniqueFileName(archivePath + "toarchive/" + archive.createFilename(unicode(title)) + ".iso")
# add " conv=notrunc,noerror"?
cmd = "dd if=%s of='%s' %s" % (self.device, path, options)
# print(cmd)
res = btec(cmd)
def getAlbumItem(self):
"""Return album item (user selects if there are several), or None.
"""
albums = []; strings = []; i = 1
for item in traverseRecursive(self.invisibleRootItem()):
if getText(item, self.levelColumn) == "album":
albums.append(item)
strings.append("%d: %s" % (i, getText(item, self.dbTitleColumn)))
i += 1
if len(albums) == 1: return albums[0]
s = StringSelector(strings)
if not s.exec_(): return
index = int(getId(s.selection()[0])) - 1
# print "index:", index
return albums[index]
def getCurrentAlbumItem(self):
"""Return current album item, or None.
"""
albums = []; strings = []; i = 1
for item in traverseRecursive(self.invisibleRootItem()):
if getText(item, self.levelColumn) == "album" and getText(item, self.curColumn) == ">":
albums.append(item)
if len(albums) == 1: return albums[0]
errmsg("More than one album marked as current!", "AlbumForm")
# def createAlbumCopy(self):
# """Create audio CD-R[W] from audio CD, DVD+-R[W] from video DVD, etc.
# TODO: Currently works for audio CDs from MP3 files only.
# 1. Find album (object) with getText(item, self.curColumn) == ">"
# 2. Call albumCopyCreater(objectId)
# albumCopyCreater does:
# 1. Decide which source(s) to use.
# There are sources for the whole album (e.g. DVD ISO file, MP3WRAP)
# or single documents (e.g. tracks).
# Sources may be more (e.g. MP3) or less easily reconvertible to semi-digital (WAV) format.
# 2. Make sources available.
# 3. Convert sources if necessary.
# 4. Combine them to a CD/DVD image.
# 5. Burn the image (one or more times).
# """
# caption = "createAlbumCopy"
## albumItem = self.getAlbumItem()
## if not albumItem: return
# h = self.hierarchyWidget
## msgbox("self.objectId" + self.objectId)
## object.reproduceAlbum(self.objectId)
# tools.reproduceAlbum(self.objectId)
# return
# documentidList = []
# for item in traverseRecursive(self.invisibleRootItem()):
# if unicode(item.text(self.levelColumn)) == "document": # and getText(item, self.curColumn) == ">":
# documentidList.append(getText(item, self.idColumn))
# # see http://www.faqs.org/docs/Linux-mini/MP3-CD-Burning.html
# print documentidList
# basedir = "/unibas_archive/tmp"
# outdir = basedir + "/cd/"
# btc("mkdir " + outdir)
# for (i0, documentId) in enumerate(documentidList):
# (versionId, mp3file) = document.getDocumentVersion(documentId)
# trackpath = "%strack%02d.wav" % (outdir, i0+1)
# # --rate 44100 --stereo --buffer 3072 --resync
# cmd = "lame --decode '%s' '%s'" % (mp3file, trackpath)
# print cmd
# btc(cmd)
#
# msgbox(outdir + " finished.")
#
# burnCmd = 'wodim -v -pad gracetime=2 dev=/dev/scd0 -audio '
# fl = getFileList(outdir); fl.sort(); trackCount = len(fl)
# for file in fl:
# burnCmd += '"%s/%s" ' % (outdir,file)
# # Output example (to stdout):
# # Track 01: 7 of 87 MB written (fifo 100%) [buf 99%] 10.6x.
# print "burnCmd:" , burnCmd
# pp = ProcessProgress(burnCmd, windowTitle=caption,
# labelText="Burning CD",
# outRegExpString=".*Track\\s*(\\d+):\\s*(\\d+)\\s*of\\s*(\\d+)\\s*MB.*")
# pp.extractExpr = "100*((float(m.group(1))-1)+float(m.group(2))/float(m.group(3))) / %d" % trackCount
# pp.start()
# if QMessageBox.question(None, caption, "Delete %s?" % outdir,
# QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No)) \
# == QMessageBox.No: return
# btc('rm -rf "%s"' % outdir)
def createMp3wrap(self):
"""
"""
caption = "createMp3wrap"
#albumItem = self.getCurrentAlbumItem()
albumItem = self.currentItem()
if not albumItem: return
if getText(albumItem, self.levelColumn) != "album":
errmsg("Please select an album item!", caption)
return
#h = self.hierarchyWidget
albumTitle = getText(albumItem, self.dbTitleColumn)
msgbox(albumTitle)
documentidList = []
# for item in traverseRecursive(self.invisibleRootItem()):
for item in traverseRecursive(albumItem):
# if getText(item, self.levelColumn) == "document" and getText(item, self.curColumn) == ">":
if getText(item, self.levelColumn) == "document":
documentidList.append(getText(item, self.idColumn))
# see http://www.faqs.org/docs/Linux-mini/MP3-CD-Burning.html
# print(documentidList)
dest = uniqueFileName("/unibas_archive/export/" + archive.createFilename(albumTitle))
cmd = "mp3wrap '%s' " % dest
for (i0, documentId) in enumerate(documentidList):
(versionId, mp3file) = document.getDocumentVersion(documentId)
cmd += "'%s' " % mp3file
# print(cmd)
btc(cmd)
fulldest = dest + "_MP3WRAP.mp3"
if os.path.isfile(fulldest):
msgbox("successfully created: " + fulldest, caption)
else:
errmsg("Failed to create: " + fulldest, caption)
# AlbumHierarchy fillFields() and its helper functions
def fillFields(self, objectId=None, productId=None, seriesId=None, docsToA=None):
"""Display data from given album (or package) in hierarchyWidget.
May be called:
- "empty", i.e. without any disc or directory to archive
- when archiving a CD/DVD
- when importing a directory
Can be called with:
- series
- package without object (on package level)
- album
One of these must be given.
TODO: if medium product is known, but its record has no discid yet,
add discid; example: Asterix in America
"""
self.invisibleRootItem().takeChildren()
self.setHeaderLabels(self.baseColumnNames)
if objectId: self.objectId = objectId
if productId and not objectId:
(objectId, descr) = dbq.getOrigObjectOfProduct(productId, objectId)
mainOt = otHead(self.ot or '')
log = logging.getLogger("fillFields")
series = Series(objectId, productId, seriesId, mainOt)
seriesItem = self.regItem(None, "series", series.getName(), id=seriesId)
if not docsToA: docsToA = DocsToArchive()
self.docsToA = docsToA
for se in series.getElements(): # Loop over episodes.
# log.debug("cnt=%s, objectId=%s, productId=%s" % (eCnt, eObjectId, eProductId))
package = Package(se.objectId, se.productId)
packageItem = self.regItem(seriesItem, "package", package.getName(),
id=package.getId(), rn=se.cnt)
for pe in package.getElements():
album = pe.getObjectDic()
if not album: continue
objectId = album["id"]
# if pe.getMaintype(): self.maintype = pe.getMaintype()
self.maintype = pe.getMaintype() or self.maintype
origDocs = docsToA.getOrig(pe.cnt)
copyDocs = docsToA.getCopy(pe.cnt)
cur = '*' if origDocs else '>' if objectId == self.objectId else ' '
albumItem = self.regItem(packageItem, "album", album["descr"], cur=cur,
id=objectId, rn=pe.cnt if package.getId() else se.cnt)
self.showMediaCount(pe.getProduct().getId(), albumItem)
self.setupFormatColumns(package.getVersionMimetypeIds(pe.cnt))
self.displayDocumentsOfObject(objectId, albumItem,
origDocs, copyDocs, album, cur)
self.setColWidths() # also sets format columns
if self.invisibleRootItem().childCount(): # to display a document if there is...
self.setCurrentItem(self.invisibleRootItem().child(0))
# Propose to schedule and execute copy actions.
if self.discid and objectId: # albumList[0]:
newCopies = self.scheduleDefaultActions(docsToA)
if newCopies > 0:
if askYesNo(True, "Execute scheduled actions?"):
self.form.execScheduledActions()
def regItem(self, parent, level, dbTitle=None, file=None, cur=' ',
rn=None, dn=None, id=None, extTitle=None, rnFormat="%02d"):
"""Register and return item of the hierarchy (package, album etc.).
If id is None just return parent.
Arguments:
parent: None if this item is top level, parent item otherwise
level: text to show in level column
title: text to show in title column
rn: running number per parent level, integer counter value
dn: document number per album object, max 2 digits
id: id of original document (not product); if None just return parent
extTitle: title from FreeDB or other external infosource
"""
if not id: return parent
item = QTreeWidgetItem(parent)
if not parent: self.insertTopLevelItem(0, item)
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable)
item.setText(self.levelColumn, level)
item.setExpanded(True)
if level == "album":
item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator) # ChildIndicatorPolicy
item.setExpanded(cur=='>')
if dbTitle: item.setText(self.dbTitleColumn, dbTitle)
if extTitle: item.setText(self.extTitleColumn, extTitle)
if file: item.setText(self.fileColumn, file)
item.setText(self.curColumn, cur)
if rn: item.setText(self.rnColumn, rnFormat % rn)
if dn: item.setText(self.dnColumn, "%02d" % dn)
if id: item.setText(self.idColumn, id)
color = self.backgroundColor[level]
if color:
for c in range(self.fileColumn + 1):
item.setBackgroundColor(c, QColor(color))
return item
def showMediaCount(self, productId, album):
"""Show how many own media of this product the user has."""
if productId:
product = getEntityDic("product", productId)
self.baseColumnNames[self.albumColumn] = product["ot"] or "Album"
self.setHeaderLabels(self.baseColumnNames)
ownMediaCount = db.readSql("SELECT count(*) FROM object WHERE productid=%s AND roomid IS NOT NULL" % productId)
if int(ownMediaCount) > 0: album.setText(self.albumColumn, ownMediaCount)
def setupFormatColumnsFromFiles(self, dir, filelist):
self.setupFormatColumns(self.getFormatIds(dir, filelist))
def setupFormatColumns(self, newids):
"""Add newids to the set of formats and setup format columns.
Create the new formats and columns incl. captions.
Argument: list of mimetype IDs
"""
log = logging.getLogger("setupFormatColumns")
log.debug("self.formats: %s" % unicode([f.caption() for f in self.formats]))
log.info("baseColumnNames: %s" % unicode(self.baseColumnNames))
ids = set([f.getId() for f in self.formats]) # Existing mimetype IDs
log.debug("ids: %s" % unicode(ids))
log.debug("newids: %s" % unicode(ids))
nextCol = self.formatColumnStart + len(ids) # Start column for new IDs
for id in newids: # Loop through IDs some of which may have a column already
# format = mimetype.mimetypeFromFile(dir+'/'+path)
# id = format.getId()
if id not in ids: # Really new, not yet in ids
log.info("New format id: %s" % id)
# print "new id:", id
ids.add(id)
format = mimetype.Mimetype(id)
format.readFromDb()
# self.formatColumn[id] = nextCol; nextCol += 1
self.formatColumn[id] = nextCol; nextCol += 1
self.formats.append(format)
self.columnNames = []; self.columnNames.extend(self.baseColumnNames)
# self.columnNames = self.baseColumnNames # WARNING: this fails, they are pointers to same object
for format in self.formats: self.columnNames.append(format.caption())
log.info("columnNames: %s" % unicode(self.columnNames))
self.columnCount = self.baseColumnCount + len(self.formats)
self.setColumnCount(self.columnCount)
self.setHeaderLabels(self.columnNames)
for i in range(len(self.formats)):
self.setColumnWidth(self.formatColumnStart + i, 30)
def addFormatColumn(self, id):
"""Add a single format column given by ID."""
#h = self #.hierarchyWidget
ids = set([f.getId() for f in self.formats])
if id in ids: return
nextCol = self.formatColumnStart + len(ids)
format = mimetype.Mimetype(id)
ids.add(id)
format.readFromDb()
self.formatColumn[id] = nextCol
self.formats.append(format)
self.columnNames = []; self.columnNames.extend(self.baseColumnNames)
for format in self.formats: self.columnNames.append(format.caption())
self.columnCount = self.baseColumnCount + len(self.formats)
self.setColumnCount(self.columnCount)
#print "self.columnNames",self.columnNames
self.setHeaderLabels(self.columnNames)
for i in range(len(self.formats)):
self.setColumnWidth(self.formatColumnStart + i, 30)
def displayRootCopy(self, dbdocs, album, objectId):
"""If there is a document for the album as a whole,
display its number of copies per type."""
for d in dbdocs:
if d["file"] != '/': continue
album.setText(self.fileColumn, '/')
# As album object ID is needed, it is stored in ID field, not the root document ID.
# The root document ID must be read from DB if needed.
fSql = "SELECT distinct(copytypeid), count(*) FROM v_albumversion WHERE albumid=" + objectId
fSql += " AND origid=%s GROUP BY copytypeid;" % d["documentid"]
for f in db.dicList(fSql):
album.setText(self.formatColumn[f["copytypeid"]], f["count"])
return
def displayDocumentsOfObject(self, objectId, albumItem, origDocs, copyDocs, objectDic, cur):
"""Given an albumItem and its objectId,
display its documents, and sides and works if there are.
Used by fillFields().
origDocs: list of original (non-archivable) documents
copyDocs: list of copy documents, e.g. MP3
TODO: Also handle level "work"? How to recognize it?
"""
log = logging.getLogger("displayDocumentsOfObject")
h = self
dbdocs = db.dicList("SELECT * FROM document d, cp WHERE d.id=cp.documentid "
"AND cp.objectid=%s ORDER BY cp.file, cp.id" % objectId)
# dbdocs: dicList of documents read from DB;
# these are level "document" or "album" (if album, file=='/')
self.displayRootCopy(dbdocs, albumItem, objectId)
sided = objectDic['ot'] in ('MC', 'LP')
if sided:
log.info("This album has sides A and B")
oldside = 'X' # Start with no side, to detect beginning of side A.
sides = ['A','B']
log.debug("album: " + unicode(albumItem))
log.debug("dbdocs: " + unicode(dbdocs))
# log.debug("copyDocs: " + unicode(copyDocs))
workCnt = cnt = extidx = 0; oldWorkId = None
albumdocs = [d for d in dbdocs if d["file"] == '/']
# maxCnt = max(len(dbdocs)-len(albumdocs), len(extTitleList)) + len(albumdocs)
maxCnt = max(len(dbdocs) - len(albumdocs), len(copyDocs)) + len(albumdocs)
log.debug("maxCnt: " + unicode(maxCnt))
rn = 0
for i in range(maxCnt): # index of dbdocs, 0-based loop over documents
i1 = i + 1 # 1-based
# dbdoc is current document if there is one for this index
# (index may be higher if extTitleList is longer).
dbdoc = dbdocs[i] if len(dbdocs)>=i+1 else None # formerly d
src = copyDocs[i].fileName() if i < len(copyDocs) and copyDocs[i] else ''
file = dbdoc.get("file") if dbdoc else src
log.info("file: %s" % file)
if file == '/': continue # Root is handled above.
cnt += 1 # Counter per side, 1-based
documentId = dbdoc.get("documentid") if dbdoc else None
dbTitle = dbdoc.get("title") if dbdoc else ''
log.debug("i=%d cnt=%d documentId=%s dbTitle='%s'" % (i,cnt,documentId,dbTitle))
if sided:
side = file[0] if file else '-'
if side not in sides: side = 'A'
print "side:", side
if oldside != side: # Detect side change, also beginning of first side.
rn = 0 # Reset counter per side.
# RN as Running Number, counts elements of next-higher hierarchy
# DN as Document Number, counts documents of album
# different only if there are sides A/B or works
# TODO: make all fields read-only;
# edit name field only on context menu request
sideItem = self.regItem(albumItem, "side", side, cur=cur, id=side)
oldside = side
parent = sideItem
else:
parent = albumItem
# Check if this document is part of a container (e.g. of a composition)
containerList = document.getContainerDocuments(documentId) if documentId else None
workId = containerList[0] if containerList else None
if workId:
if workId != oldWorkId:
#rn = 0
#oldWorkId = workId
#workCnt += 1
rn, oldWorkId, workCnt = 0, workId, workCnt + 1
workTitle = db.descr("document", workId)
workItem = self.regItem(parent, "work", workTitle, rn=workCnt, id=workId, cur=cur)
parent = workItem
rn += 1
# RN is workCnt if there is a work, else counter per side if sided
# DN is document counter per object
#rn = workCnt if workId else None
item = self.regItem(parent, 'document', dbTitle, file=file, rn=rn, dn=cnt, id=documentId, cur=cur)
if extidx < len(copyDocs): # Display ext. title if there is for this index.
#item.setText(self.extTitleColumn, docList[extidx][0].vd["title"])
item.setText(self.extTitleColumn, copyDocs[extidx].vd["title"])
extidx += 1 # Counter for extTitleList, 0-based
if documentId:
fSql = "SELECT distinct(copytypeid), count(*) FROM v_albumversion WHERE albumid=" + objectId
fSql += " AND origid=%s GROUP BY copytypeid;" % documentId
for f in db.dicList(fSql):
item.setText(self.formatColumn[f["copytypeid"]], f["count"])
#def displaySingleDocuments(self, albumItem, dbdocs, origDocs, copyDocs, object, objectId, cur):
#"""Display documents in an albumItem (lowest level).
#dbdocs: dicList of documents read from DB;
#these are level "document" or "album" (if album, file=='/')
#docList: list of (orig,copy) Document tuples for tracks etc. on disc
#TODO: Also handle level "work"? How to recognize it?
#"""
#log = logging.getLogger("displaySingleDocuments")
#h = self
#print "=== === === === === "
#sided = object['ot'] in ('MC', 'LP')
#print "sided:", sided
#if sided:
#log.info("This album has sides A and B")
#oldside = 'X' # Start with no side, to detect beginning of side A.
#sides = ['A','B']
#log.debug("album: " + unicode(albumItem))
#log.debug("dbdocs: " + unicode(dbdocs))
## log.debug("copyDocs: " + unicode(copyDocs))
#workCnt = cnt = extidx = 0; oldWorkId = None
#albumdocs = [d for d in dbdocs if d["file"]=='/']
## maxCnt = max(len(dbdocs)-len(albumdocs), len(extTitleList)) + len(albumdocs)
#maxCnt = max(len(dbdocs)-len(albumdocs), len(copyDocs)) + len(albumdocs)
#log.debug("maxCnt: " + unicode(maxCnt))
#for i in range(maxCnt): # index of dbdocs, 0-based loop over documents
#i1 = i + 1 # 1-based
## dbdoc is current document if there is one for this index
## (index may be higher if extTitleList is longer).
#dbdoc = dbdocs[i] if len(dbdocs)>=i+1 else None # formerly d
#src = copyDocs[i].fileName() if i < len(copyDocs) and copyDocs[i] else ''
#file = dbdoc.get("file") if dbdoc else src
#log.info("file: %s" % file)
#if file == '/': continue # Root is handled above.
#cnt += 1 # Counter per side, 1-based
#documentId = dbdoc.get("documentid") if dbdoc else None
#dbTitle = dbdoc.get("title") if dbdoc else ''
#log.debug("i=%d cnt=%d documentId=%s dbTitle='%s'" % (i,cnt,documentId,dbTitle))
#print "sided2:", sided
#if sided:
#side = file[0] if file else '-'
#if side not in sides: side = 'A'
#print "side:", side
#if oldside != side: # Detect side change, also beginning of first side.
##cnt = 1 # Reset counter per side.
## TODO: decide if cnt is abs. or rel. per side
## must be abs if used as counter for imported files
## maybe both abs and rel counter?
## Proposal:
## RN as Running Number, counts elements of next-higher hierarchy
## D# or DN as Document Number, counts documents of album
## TODO: make all fields except title read-only;
## TODO: react on title change automatically after ENTER
#sideItem = self.regItem(albumItem, "side", side, cur=cur, id=side)
#oldside = side
#parent = sideItem
#else:
#parent = albumItem
## Check if this document is part of a container (e.g. of a composition)
#containerList = document.getContainerDocuments(documentId) if documentId else None
#workId = containerList[0] if containerList else None
#if workId:
#if workId != oldWorkId:
#oldWorkId = workId
#workCnt += 1
#workTitle = db.descr("document", workId)
#workItem = self.regItem(parent, "work", workTitle, id=workId, cur=cur)
#parent = workItem
#item = self.regItem(parent, 'document', dbTitle, file=file, cnt=cnt, id=documentId, cur=cur)
#if extidx < len(copyDocs): # Display ext. title if there is for this index.
##item.setText(self.extTitleColumn, docList[extidx][0].vd["title"])
#item.setText(self.extTitleColumn, copyDocs[extidx].vd["title"])
#extidx += 1 # Counter for extTitleList, 0-based
#if documentId:
#fSql = "SELECT distinct(copytypeid), count(*) FROM v_albumversion WHERE albumid=" + objectId
#fSql += " AND origid=%s GROUP BY copytypeid;" % documentId
#for f in db.dicList(fSql):
#item.setText(self.formatColumn[f["copytypeid"]], f["count"])
def setColWidths(self):
h = self #.hierarchyWidget
# if not self.maintype:
# self.maintype = (self.productWidget.getValue("rt") or '').lower()
if self.maintype and self.maintype in self.defaultFormats:
for f in self.defaultFormats[self.maintype]:
# print "self.addFormatColumn(%s)" % f
self.addFormatColumn(f)
for i in range(len(self.baseColumnWidths)):
name = self.baseColumnNames[i]
width = self.baseColumnWidths[i]
if name == "External title" and not (self.discid or self.dirToImport):
width = 30
self.setColumnWidth(i, width)
#def getSeriesObjectProductList(self, thisObjectId, thisProductId, thisProduct):
#"""Get a list of tuples (counter, objectId, productId, None)
#if a document on medium thisObjectId is an episode of a document series.
#The tuple members are:
#counter: ord field in the episode rel
#objectId: ID of album carrying an episode
#productId: ID of product of album
#None: placeholder for package ID
#Currently, series containing packages are not yet handled (TODO).
#Albums in packages in a series are shown without package.
#If the current album is not episode of a series, then
#the normal getObjectProductList() function is called which also handles packages.
#"""
#mainOt = otHead(self.ot or '')
#seriesId = dbq.getSeriesIdFromAlbum(thisObjectId)
#if not seriesId: return self.getObjectProductList(thisObjectId, thisProductId, thisProduct)
#self.seriesItem = self.regItem(None, "series", db.descr("document", seriesId), id=seriesId)
#return dbq.getEpisodeObjectList(seriesId, mainOt)
#def getObjectProductList(self, thisObjectId, thisProductId, thisProduct):
#"""Get a list of tuples (counter, objectId, productId, packageId) for non-series episodes.
#Listed are all parts of the package this album is part of.
#If there is no package, the list contains only this album.
#The tuple members are:
#counter: order of package members
#objectId: ID of album in package, or current album ID
#productId: ID of product of album, or None if there is no product
#packageId: ID of package this album is part of, or None
#"""
#log = logging.getLogger("getObjectProductList")
#log.debug("thisObjectId:"+ thisObjectId)
#self.seriesItem = None # Used as parent item.
#prods = db.dicList("SELECT * FROM product WHERE packageid=%s" % thisProductId) if thisProductId else None
#if prods:
#package = getEntityDic("product", thisProductId)
#else:
#package = getEntityDic("product", packageId) # Package details or None.
#if thisProductId: # There is a product.
#log.debug("thisProductId:"+ thisProductId)
#if package: # There is also a package. Produce a list of package members.
#packageId = package["id"]
#log.debug("packageId:"+ packageId)
#(e,l) = db.getPackage(packageId, thisObjectId, thisProductId)
#if e: errmsg(e)
#objectProductList = [] # Now create final list with 1-based counter.
#for (i0, (objectId, productId, z)) in enumerate(l): # z was only needed for sorting.
#objectProductList.append((i0+1, objectId, productId, packageId))
#else: # No package. Return one-element list of this album with its product.
#objectProductList = [(1, thisObjectId, thisProductId, None)]
#else: # No product. Return one-element list of this album without product.
#objectProductList = [(1, thisObjectId, None, None)]
#return objectProductList
def setImportPackage(self):
"""For importDirectory() get package info from self.dirToImport.
Set self.package and self.albumDirList."""
self.package = None
dirIsPackage = False
subdirList = getSubdirList(self.dirToImport) or [] # Sorted subdirectories ...
# ... which are the parts of a package unless they are "AUDIO_TS", "VIDEO_TS" of a video DVD ...
if subdirList: dirIsPackage = not isDvdStructure(self.dirToImport, self.ot) # ... which is checked here.
# log.info("dirIsPackage: %s" % dirIsPackage)
if dirIsPackage: # Now prepare package and albumDirList which contains album directories.
self.albumDirList = [self.dirToImport + '/' + d for d in subdirList] # Sorted list of parts of package.
packageId = self.getDirPackageId(self.dirToImport, subdirList)
if not packageId: return
# packageOt = "%d %ss" % (len(subdirList), ot)
#self.package = db.idDic("product", packageId) # TODO: as Product, see below:
self.package = product.Product(packageId)
self.package.readFromDb()
#packageTitle = package.vd["name"]
#log.info("Selected package ID: %s" % packageId)
askAndRecordProductImage(self.ot, self.dirToImport, packageId)
else: # User-selected directory to archive is not a package but a single album.
self.albumDirList = [self.dirToImport] # Turn single album into one-element list with no package.
def setImportProduct(self, albumDir):
"""For importDirectory() get product number self.prodNo.
Return productId which may be None if user aborts."""
self.otNo = "%s %d" % (self.ot, self.prodNo) # E.g. "CD 1"; ot is parent dir e.g. "MC", "CD", "DVD"
# productDirOnly = albumDir.split('/')[-1] # dirname, in packages this may be just "CD 1", "CD 2" etc.
productDic = readProductDir(albumDir) # Extract product info from dir name.
if "title" in productDic:
print "productDic['title']", productDic['title']
productDic['title'] = productDic['title'].replace('_', ' ')
print "productDic['title']", productDic['title']
productDic["ot"] = self.ot
if self.ot == "DVD":
productDic["rt"] = "Video"
elif self.ot in ("CD","MC","LP"):
productDic["rt"] = "Audio"
elif self.ot == "book":
productDic["rt"] = "text"
if self.ot == "DVD" and productDic["rt"] == "Video":
# Decide if albumDir is a DVD directory structure; in that case, normalize it.
print("albumDir", albumDir)
if os.path.exists(albumDir + "/video_ts") or os.path.exists(albumDir + "/VIDEO_TS"):
print("upcaseDvdDir")
upcaseDvdDir(albumDir)
if not "title" in productDic or productDic["title"] == self.otNo: # If no (good) title is given ...
albumTitle = albumDir.split('/')[-1] or "(Unknown)" # This may be just "CD 1" etc., in this case better...
if self.package: albumTitle = "%s %s" % (self.package.vd["name"], albumTitle) # ... prepend packageTitle
#print "prepend packageTitle =>", albumTitle
productDic["title"] = albumTitle
# log.info("%s %d (1-based) of %d: %s" % (ot, i, len(albumDirList), albumTitle))
if self.package: productDic["packageid"] = self.package.vd["id"]
productId = self.setProduct(productDic, productDic["title"], self.prodNo) # Prepare, ask and evaluate product.
self.productDic = productDic
return productId
def handleImportDvd(self, albumDir):
"""For importDirectory() handle a directory that contains a video DVD."""
log = logging.getLogger("handleImportDvd")
# h = self.hierarchyWidget
rootDocDic = db.getRootDocDic(self.objectId)
# offer user one default and maybe several alternative supported formats
# to select or confirm as copy formats TODO
if rootDocDic: # A root document exists already.
origDoc = document.Document(initDic=rootDocDic, src='/')
# docId = rootDocDic["id"]
# orig.vd = db.idDic("document", docId)
else:
docDic = readProductDir(albumDir, src='/') # Extract doc info from dirname.
origDoc = document.Document(initDic=docDic) # Create (original, non-archivable) document.
#docDic.update(orig.vd)
origDoc.vd["licenseid"] = '13' # DVD is usually copy protected
origDoc.vd["mimetypeid"] = '122' # non-archivable original DVD content
copyDoc = deepcopy(origDoc)
copyDoc.vd["mimetypeid"] = "122" # it is still a directory, only after genisoimage it's an ISO file
copyDoc.src = albumDir
print("in handleImportDvd: copyDoc.src=", copyDoc.src)
# log.info("orig: %s" % unicode(orig))
# log.info("copyDoc: %s" % unicode(copyDoc))
return (origDoc, copyDoc)
def handleImportDoc(self, j, file, fileList, albumDir, albumTitle):
"""Handles a single file in a directory to import; called by importDirectory.
j: 1-based file index
Produces two Documents: oridDoc and copyDoc, mostly identical,
except for mimetype, hash.
"""
# TODO: Show progress in MultiProgressDialog, by file number on lowest level.
# (Not file size because here we only prepare copies.)
#print("file %d of %d" % (j, len(fileList)))
path = albumDir + '/' + file # Complete (source) file path.
schema = "%%s %%%dd" % len(unicode(len(fileList))) # "%s %2d" if there are >=10 files
# log.info("File no. %d: %s" % (j, path))
# hash = archive.hash(path) # Hash of copy to check if it is already known.
#print("* * * Getting hash of "+path)
# TODO: problem: we have hash for file, but not for directory where we move the file
# To save time, we should store the hash and use it if file is moved unchanged.
hash = archive.smarthash(path) # Hash of copy to check if it is already known.
#print("hash:", hash)
dl = db.dicList("SELECT * FROM document WHERE hash='%s'" % hash)
if dl: # Copy already known. TODO: More thoroughly think of what to do in this case.
docDic = dl[0]
origDoc = document.Document(initDic=docDic) # Create (original, non-archivable) document.
msg = "File '%s' is already archived as document:\n%s\n" % (file, db.descrWithId("document", docDic["id"]))
# if not docDic["mimetype"]: msg += "\nBut has no mimetype!"
msg += "Mimetype: %s" % unicode(docDic["mimetypeid"])
# log.warn(msg)
msgbox(msg)
#docDic["copymtid"] = docDic["mimetypeid"]
else:
docDic = readDocFilename(file, schema % (albumTitle, j)) # Extract doc info from filename.
#if singleDoc: docDic["file"] = '/'
origDoc = document.Document(initDic=docDic) # Create (original, non-archivable) document.
attributes = dict() # Will be filled by metadatareader.
metadatareader.readFromFile(attributes, path)
metadatareader.selectMaxLikelihood(attributes)
origDoc.getMetadataValues(attributes)
# print '== == == origDoc.vd["mimetypeid"]', origDoc.vd["mimetypeid"]
copyDoc = deepcopy(origDoc)
# Mimetype is that of copy. Original mimetype (e.g. "SO", "MP") is derived here from it.
if not "mimetypeid" in origDoc.vd: origDoc.vd["mimetypeid"] = None
mimetypeId = copyDoc.vd["mimetypeid"] = origDoc.vd["mimetypeid"]
# Every format, whether supported or not, can be handled in a way
# that a file in this format can be archived, i.e. copied to the archive
# and provided with basic metadata (title etc.).
# So if a mimetype (=format) is recognized, we must ensure that
# there is a format column for it.
if mimetypeId:
#print "mimetypeId", mimetypeId
self.setupFormatColumns([mimetypeId])
mt = mimetype.Mimetype(mimetypeId)
if mt.maintype() == "audio" or self.ot in ("MC","LP"): origDoc.vd["mimetypeid"] = '113'
elif mt.maintype() == "video" and self.ot == "DVD": origDoc.vd["mimetypeid"] = '122'
# log.info("orig mimetypeid: %s; copy mimetypeid: %s" % (docDic["mimetypeid"], mimetypeId))
#print docDic
copyDoc.src = path
# print ">>>>>>>>>> copyDoc:", copyDoc
#docDic["albumDir"] = albumDir
#docDic["file"] = file
return (origDoc, copyDoc)
def importFileAlbum(self):
"""Archive an album stored in a file.
The file represents the product, object and root document.
Create a directory, move file into it and
use importDirectory() for this.
"""
caption = "Import file album"
#h = self.hierarchyWidget
log = logging.getLogger("importFileAlbum")
f = selectFile(self.lastImportedType)
if not f: return
filename = os.path.basename(f)
dirpath = uniqueFileName(os.path.splitext(f)[0])
msg = textwrap.dedent("""
<html><h3>Import File Album</h3>
<p>This function imports single file albums from HD.
It is only a wrapper for the more general ImportDirectory function.
it creates a directory with the single file in it
and calls the ImportDirectory function.</p>
<p>Should this function fail, you will find the file
in this directory:</p>
<blockquote>%s</blockquote>
<p>To try again importing it, use the ImportDirectory function
directly then.</p></html>
""")[1:-1] % dirpath
msgbox(msg, caption)
os.mkdir(dirpath)
newname = "%s/%s" % (dirpath, filename)
os.rename(f, newname)
# If there is a rawfilelist entry for this file, ensure it points
# to the file at the new location.
sql = "UPDATE rawfilelist SET file=%s WHERE file=%s AND host=%s" \
% (q(os.path.realpath(newname)), q(os.path.realpath(f)), q(getHost()))
print sql
db.execSql(sql)
self.importDirectory(dirpath)
def importDirectoryDialog(self):
"""Dialog to input directory to import using importDirectory()."""
dirToImport = selectDir(self.lastImportedType)
if not dirToImport: return
if dirToImport[-1] == '/': dirToImport = dirToImport[:-1]
self.importDirectory(dirToImport)
def importDirectory(self, dirToImport):
"""Archive an album stored in a directory that conforms to specs in node 3401.
Archiving happens in several steps. This is only the first one:
1. (This one): Create DB entries of original docs, remember file paths
This corresponds to showDiscMetadata but they cannot be merged.
Result: product, object and (original) document information in the DB.
At the end, fillFields() is called.
If user agrees, commitImport() is called.
Next see proposals
TODO: Make sure mimetype and license info is stored.
copy contype, license, length, languageid
TODO: DVDs can be imported as ISO files. Don't create directory structure in this case.
"""
caption = "Import directory"
log = logging.getLogger("importDirectory")
self.clear()
self.dirToImport = dirToImport
log.info("Selected directory to import: %s" % dirToImport)
self.ot = ot = parentDir = dirToImport.split('/')[-2] # Parent directory named "CD", "DVD", "MC" or so
self.setImportPackage() # Set self.package and self.albumDirList
log.info("self.package: %s" % unicode(self.package))
log.info("self.albumDirList: %s" % self.albumDirList)
objectId1 = None # Will hold first object ID.
docsToA = DocsToArchive()
# Loop through (possibly 1-element) album list.
for i0, albumDir in enumerate(self.albumDirList):
self.prodNo = i = i0 + 1 # 1-based album counter
# TODO: Make sure that with packages, always the right product is preselected.
productId = self.setImportProduct(albumDir) # sets self.productDic
if not productId: return
askAndRecordProductImage(ot, albumDir, productId) # TODO: think more thoroughly
objectDic = {"ot":ot, "dt":getDimensionType(ot), "virtual":"true",
"descr":albumDir.split('/')[-1], "orig":"true", "productid":productId}
albumTitle = self.productDic["name"]
objectId = self.setObject(objectDic, albumTitle) # Prepare, ask and evaluate object.
if not objectId: return # TODO: in this case stop here and don't create documents
self.objectId = objectId
if not objectId1: objectId1 = objectId # First objectId needed for self.objectId.
#if not self.package: self.objectWidget.setId(objectId)
if isDvdStructure(albumDir):
docsToA.setPair(i, albumList.append([self.handleImportDvd(albumDir)])) # there is only one tuple in the list
else: # Every file is a document
fileList = getFileList(albumDir)
# albumList.append([]) # create albumList[i0]
for (j0, file) in enumerate(fileList): # Loop through files sorted alphabetically.
j = j0 + 1 # 1-based file index
#(origDoc, copyDoc) = self.handleImportDoc(j, file, fileList, albumDir, albumTitle)
docsToA.addPair(i, self.handleImportDoc(j, file, fileList, albumDir, albumTitle))
# albumList[i0].append((orig, copy))
self.objectId = objectId1 # fillFields() needs an objectId to display
self.lastImportedType = ot
# docsToA.pprint()
self.fillFields(objectId=objectId1, docsToA=docsToA)
# To reproduce e.g. a video DVD:
# In AlbumForm, find the album
# File - Reproduce album
# If the copy should get a number, say Yes to "Create new object ib DB?", and
# select the room for this new object
# Note the number and descr of the new object
# Insert the object (maybe after writing number and descr on it)
def reproduceAlbum(self):
tools.reproduceAlbum(self.objectId)
def getDirPackageId(self, dir, subdirList):
"""Return product ID of package. Used in importDirectory.
"""
log = logging.getLogger("getDirPackageId")
package = product.Product(initDic=readProductDir(dir)) # Extract information from directory name.
log.info("package: %s" % unicode(package))
albums = len(subdirList)
ot = parentDir = dir.split('/')[-2] # Parent directory named "CD", "DVD", "MC" etc.
package.vd["ot"] = "%d %ss" % (albums, ot) # Add derived information e.g. ot="2 CDs"
packageId = package.vd["id"] if "id" in package.vd else None
pid = getProductidFromObjectidInDic(package) # If objectid is known, try to get productid from it.
if pid: packageId = pid
if package.vd["title"].endswith(" " + package.vd["ot"]): # Remove "2 CDs" or so at end
package.vd["title"] = package.vd["title"][:-len(package.vd["ot"])-1]
if not packageId:
if not "title" in package.vd:
(text,ok) = QInputDialog.getText(self, "Enter package title",
"Please enter package title here.")
if not ok: return None
title = unicode(text)
else:
title = package.vd["title"]
critList = ["name=%s" % q(title)]
if "ean" in package.vd: critList.append("ean=%s" % q(package.vd["ean"]))
# if self.discid: critList.append("discid=%s" % self.discid)
crit = " OR ".join(critList)
msg = "<html><b>Please select package</b></html>"
f = finddialog.FindEntityAndTypeDialog("product", msg=msg)
f.helpIntro = "<p>Please select or create a package</p>"
f.critEdit.setText(crit)
f.initDic = package.vd
f.newRecordValue = title
f.showCandidates()
packageId = f.selectedId() if f.exec_() else None
return packageId
#def setProduct(self, productDic, albumTitle=None, partno=None): #, origDocTitleList=None):
#"""Prepare, ask for and evaluate user's decision about album product.
#TODO: Ask for barcode first, if given, can be ean, isxn or mediaid, see barcode
#Only return None after asking user to select product via FindDialog.
#TODO: in a package, default-select the right product!
#Arguments:
#productDic: Data gathered so far about product. Used for initDic; name is ignored.
#albumTitle: Title found out so far. Will be prepared and used as name.
#TODO:
#- If discid is known and a product with this discid exists, then
#show this product first (other candidates as well, but this preselected), and
#focus on okButton
#- If product is likely not in DB, set focus on addButton
#TODO: If filename is "NORTHANGER ABBEY.ISO" or "NORTHANGER_ABBEY.ISO"
#take NORTHANGER_ABBEY as product.volumelabel and try to get product from it.
#"""
#a = albumTitle or '(unknown)'
#log = logging.getLogger("setProduct")
##ot = productDic["ot"] if "ot" in productDic else None
##ot = ot or self.ot
#ot = productDic.get("ot") or self.ot
#partnoString = " %d" % partno if partno else ''
#ean = productDic.get("ean")
#productIdListByDiscid = db.idListByFieldFromDic("product", "discid", productDic)
#productId = productDic.get("id")
#if not albumTitle: albumTitle = productDic.get("name")
#if not productId and "objectid" in productDic:
## dl = db.dicList("SELECT productid, descr FROM object WHERE id=" + productDic["objectid"])
#d = db.idDic("object", productDic["objectid"])
#if not d:
## msg = "Album dir %s refers to nonexisting objectid %s" % (albumDir, productDic["objectid"])
#errmsg("No object with id: %s" % productDic["objectid"])
#return None
#productId, albumTitle = d["productid"], d["descr"]
#if not productId and productDic.get("packageid"):
#packageId = productDic["packageid"]
#package = db.idDic("product", packageId)
#if package:
#albumTitle = package["name"]
#if partno:
#parts = getNum(package["ot"]) or 0
#schema = " %s %d" if parts < 10 else " %s %02d"
#albumTitle += schema % (ot, partno) #" %s %d" % (ot, partno)
#crit = "!packageid=%s" % packageId
#msg = "<p><b>Please select %s%s of package:<br>%s<br>that corresponds to this album to import:<br>%s</p></b>" \
#% (ot, partnoString, package["name"], a)
#f = finddialog.FindDialog("product", msg=msg)
## f.order = "levenshtein(name,%s)" % q(a) # TODO: autom. install and activate
#f.newRecordValue = albumTitle
##f.order = "name"
#partnoString = ' ' + unicode(partno) if partno else ''
#f.critEdit.setText(crit)
#f.initDic = productDic
## f.initDic.update({"name":"%s %s%s" % (package["name"], ot, partnoString)})
#f.showCandidates()
## print "productIdListByDiscid", productIdListByDiscid
#if not productIdListByDiscid: #"discid" in productDic and not "ean" in productDic:
## msgbox("f.addNewButton.setFocus()")
#f.addNewButton.setFocus()
#if not f.exec_(): return None
#productId = f.selectedId()
#if not productId:
#critList = []
## crit = " discid=%s" % q(self.discid) if self.discid else " false"
#if self.discid: critList.append("discid=%s" % q(self.discid))
#if 'volumelabel' in productDic:
#critList.append("volumelabel=%s" % q(productDic["volumelabel"]))
#if albumTitle and not albumTitle.startswith("(Unknown"):
#critList.append("name~*%s" % qpr(albumTitle))
#log.debug("critList: %s" % ','.join(critList))
#msg = "Please select %s %d" % (ot, partno) if partno else ''
#f = finddialog.FindDialog("product", msg=msg)
## f.helpIntro = "<p>Please select, confirm or create the product that corresponds to this %s.</p>" % ot
## f.critEdit.setText(crit)
#f.newRecordValue = albumTitle
#f.initDic = productDic
## f.showCandidates()
#f.critList = critList
#f.execCritList()
#productId = f.selectedId() if f.exec_() else None
#if not productId: return None
## From here, we have a productId. Take (maybe new) product name as albumTitle.
#albumTitle = db.idDic("product", productId)["name"]
#self.productId = productId
#if self.discid: db.setValueIfNull("product", "discid", self.discid, productId, True)
#productDic["name"] = albumTitle # needed in importDirectory()
#return productId
def setProduct(self, productDic, albumTitle=None, partno=None): #, origDocTitleList=None):
"""Prepare, ask for and evaluate user's decision about album product.
TODO: Ask for barcode first, if given, can be ean, isxn or mediaid, see barcode
Only return None after asking user to select product via FindDialog.
TODO: in a package, default-select the right product!
Arguments:
productDic: Data gathered so far about product. Used for initDic; name is ignored.
albumTitle: Title found out so far. Will be prepared and used as name.
TODO:
- If discid is known and a product with this discid exists, then
show this product first (other candidates as well, but this preselected), and
focus on okButton
- If product is likely not in DB, set focus on addButton
TODO: If filename is "NORTHANGER ABBEY.ISO" or "NORTHANGER_ABBEY.ISO"
take NORTHANGER_ABBEY as product.volumelabel and try to get product from it.
"""
a = albumTitle or '(unknown)'
log = logging.getLogger("setProduct")
#ot = productDic["ot"] if "ot" in productDic else None
#ot = ot or self.ot
ot = productDic.get("ot") or self.ot
partnoString = " %d" % partno if partno else ''
ean = productDic.get("ean")
print "ean in setProduct():", ean
productIdListByDiscid = db.idListByFieldFromDic("product", "discid", productDic)
productId = productDic.get("id")
if not albumTitle: albumTitle = productDic.get("name")
if not productId and "objectid" in productDic:
# dl = db.dicList("SELECT productid, descr FROM object WHERE id=" + productDic["objectid"])
d = db.idDic("object", productDic["objectid"])
if not d:
# msg = "Album dir %s refers to nonexisting objectid %s" % (albumDir, productDic["objectid"])
errmsg("No object with id: %s" % productDic["objectid"])
return None
productId, albumTitle = d["productid"], d["descr"]
if not productId:
critList = []
# crit = " discid=%s" % q(self.discid) if self.discid else " false"
if self.discid: critList.append("discid=%s" % q(self.discid))
vl = productDic.get('volumelabel')
if vl and len(vl) > 0:
critList.append("volumelabel=%s" % q(productDic["volumelabel"]))
if ean:
crit = "ean=%s" % q(ean)
if len(ean) < 13:
ean13 = '0' * (13 - len(ean)) + ean
crit += " OR ean=%s" % q(ean13)
critList.append(crit)
if productDic.get("packageid"):
critList.append("packageid=" + productDic.get("packageid"))
pkid = productDic.get("packageid")
if pkid: critList.append("packageid=" + pkid)
if albumTitle and not albumTitle.startswith("(Unknown"):
critList.append("name~*%s" % qpr(albumTitle))
log.debug("critList: %s" % ','.join(critList))
msg = "Please select %s %d" % (ot, partno) if partno else ''
f = finddialog.FindDialog("product", msg=msg)
# f.helpIntro = "<p>Please select, confirm or create the product that corresponds to this %s.</p>" % ot
# f.critEdit.setText(crit)
f.newRecordValue = albumTitle
f.initDic = productDic
# f.showCandidates()
f.critList = critList
f.execCritList()
productId = f.selectedId() if f.exec_() else None
if not productId: return None
# From here, we have a productId. Take (maybe new) product name as albumTitle.
albumTitle = db.idDic("product", productId)["name"]
self.productId = productId
if self.discid: db.setValueIfNull("product", "discid", self.discid, productId, True)
productDic["name"] = albumTitle # needed in importDirectory()
return productId
def setObject(self, objectDic, albumTitle):
"""Prepare, ask for and evaluate user's decision about album object.
Only return None after asking user to select product via FindDialog.
Arguments:
objectDic:
albumTitle: Title found out so far. Will be prepared and used as name.
TODO:
If there is object of given product (objectDic["productid"]), then
(pre-)select this (and maybe offer no others?).
Other candidates in order of preference:
object.descr=product.name
object.descr similar to (levenshtein) product.name
Prefer orig=True
Realizable as a list of criteria and orders, not a single one:
[("productid=%s AND orig=True" % productId, ""),
("productid=%s" % productId, ""),
("descr~*%s" % qpr(albumTitle), ""),
]
"""
#h = self.hierarchyWidget
ot = objectDic["ot"]
descr = objectDic["descr"]
if not ot: ot = self.ot
if not "dt" in objectDic: objectDic["dt"] = getDimensionType(ot)
objectDic.update({"ot":ot})
productId = objectDic.get("productid")
critList = []
# if self.objectId: crit += " OR id=" + self.objectId
if self.objectId: critList.append("id=" + self.objectId)
#crit = " productid=%s" % productId if productId else " false"
if productId:
critList.append("productid=%s AND orig=True" % productId)
critList.append("productid=%s" % productId)
if albumTitle and not albumTitle.startswith("(Unknown"):
critList.append("descr~*%s" % qpr(albumTitle))
msg = "<html><b>Please select medium object of<br>%s<br>%s</b></html>" % (albumTitle, descr)
f = finddialog.FindEntityAndTypeDialog("object", msg=msg)
f.critList = critList
# levenshtein is in fuzzystrmatch module, see http://www.postgresql.org/docs/8.3/static/fuzzystrmatch.html
# to activate it, see http://www.postgresql.org/docs/8.3/static/contrib.html
# psql -f /usr/share/postgresql/8.4/contrib/fuzzystrmatch.sql
# f.order = "levenshtein(descr,%s)" % q(descr) # TODO: could be better
f.helpIntro = "<p>Please select, confirm or create the object that corresponds to this %s.</p>" % ot
# f.critEdit.setText(crit)
#msgbox("albumTitle: %s" % albumTitle)
f.newRecordValue = albumTitle
if productId: objectDic.update({"productid":productId})
if not "orig" in objectDic: # New objects are originals by default, because
objectDic.update({"orig":"true"}) # usually one archives originals rather than copies
f.initDic = objectDic
# f.showCandidates()
f.execCritList()
objectId = f.selectedId() if f.exec_() else None
# Problem: If product is selected and later object, but object has no productid,
#it overwrites product with None because of signal.
#Solution: if object exists but has no productid, set productid before setId().
#If it already has a different productid, ask if it should be overwritten.
if objectId:
object = db.dicList("SELECT * FROM object WHERE id="+objectId)[0]
objectProductId = object["productid"]
print("productId, objectProductId:", productId , objectProductId)
#if self.discid: self.productWidget.setValueIfNull("discid", self.discid)
if productId and not objectProductId: db.execSql("UPDATE object SET productid=%s WHERE id=%s" % (productId, objectId))
else: # askAndOverwriteField(entityType, field, oldValue, newValue)
if objectProductId != productId:
msg = "Selected " + db.descrWithEntityAndId("object", objectId)
msg += "\nalready has different product %s." % db.descrWithEntityAndId("product", objectProductId)
msg += "\n\nReplace it with %s?" % db.descrWithEntityAndId("product", productId)
res = QMessageBox.question(self, "Different product", msg,
QMessageBox.StandardButtons(QMessageBox.Cancel | QMessageBox.No | QMessageBox.Yes),
QMessageBox.Yes)
if res == QMessageBox.Cancel: return
if res == QMessageBox.Yes: db.execSql("UPDATE object SET productid=%s WHERE id=%s" % (productId, objectId))
if not object["descr"] or object["descr"] == "(Unknown)":
db.execSql("UPDATE object SET descr=%s WHERE id=%s" % (q(albumTitle), objectId))
self.objectId = objectId
return objectId
def checkForPackagePart(self):
"""Return package product id and the number of the current product in this package,
after asking the user if this product is part of a package.
Return tuple:
package product id, current product number, total products in this package.
"""
log = logging.getLogger("checkForPackagePart")
h = self
# First check if the product (from self.discid) is known to be part of a package.
if self.discid:
#sql = "SELECT id, name, packageid FROM product WHERE discid=%s" % q(self.discid)
#dl = db.dicList(sql)
dl = db.dicList("SELECT id, name, packageid FROM product "
"WHERE discid={}".format(q(self.discid)))
if len(dl) == 1:
productId, packageId = dl[0]["id"], dl[0]["packageid"]
#name = dl[0]["name"]
if packageId:
# TODO: Normally we use object.descr for ordering.
# Here we use product.name which leads to a BUG
# if product.name != object.descr
#sql = "SELECT id FROM product WHERE packageid=%s ORDER BY convert_to(name, 'utf8')" % packageId
#vl = db.valueList(sql)
vl = db.valueList("SELECT id FROM product WHERE packageid={}"
" ORDER BY convert_to(name, 'utf8')".format(packageId))
if productId in vl:
i = vl.index(productId) + 1
packageDescr = db.descr("product", packageId)
# TODO: What if not true?
msg = "Product %s: %s\nseems to be the %d. part in\n%s.\nIs that true?" \
% (productId, dl[0]["name"], i, packageDescr)
# msgbox(msg) # TODO: Ask and return only if confirmed
if askYesNo(True, msg):
return (packageId, i, len(vl))
# In the last 1000 products, find those which are packages (ot='2 CDs' or so)
# and return id, name and number of parts (e.g. CDs)
sql = textwrap.dedent("""
SELECT id, ot, substring(ot from '^[0-9]+ ') as n, name
FROM (SELECT id, ot, name FROM product ORDER BY id DESC LIMIT 1000) AS prod
WHERE ot~'^[0-9]+ %s' ORDER BY id DESC LIMIT 20;
""")[1:-1] % self.ot
log.debug(sql)
lastPackagesDicList = db.dicList(sql)
sqlTrunc = "SELECT COUNT(id) FROM product WHERE packageid="
packList = [] # Gets packages that are not yet full.
for p in lastPackagesDicList: # Loop over all packages, incl. full ones.
existingParts = int(db.readSql(sqlTrunc + p["id"]))
if existingParts < int(p["n"]): # Needs to be filled, so include in selection.
s = "%s #%d of %s from %s: %s %s" \
% (self.ot, existingParts+1, p["n"], p["id"], p["ot"], p["name"])
log.debug(s)
packList.append(s)
if not packList: return (None, 1, 1) # Nothing to select from.
res = StringSelector(packList, "If this is part of a package, please select that package")
if not res.exec_(): return (None, 1, 1) # Nothing selected.
# print "Selected:", s.selection()[0]
if not res.selection(): return (None, 1, 1)
# m = re.compile(".*#([0-9]+) from ([0-9]+):.*").match(s.selection()[0])
regexpString = ".*#([0-9]+) +of +([0-9]+) +from +([0-9]+):.*"
m = re.compile(regexpString).match(res.selection()[0])
if not m:
msg = 'regexpString = %s\n' % regexpString
msg += 'does not match %s\n' % res.selection()[0]
msg += "This shouldn't happen."
print(msg)
errmsg(msg)
return (None, 1, 1) # Shouldn't happen
return (m.group(3), int(m.group(1)), int(m.group(2)))
def showDiscMetadata(self, rt, albumTitle, origDocTitleList,
volumelabel=None, ean=None, mt=None, isrcDic=None):
"""Identify package (if there is), product(s), object(s). Arguments:
rt: product.rt, i.e. "Audio", "Video", "Application" etc.,
from self.maintype.title()
albumTitle: title, usually indentical with product.name and document.title
origDocTitleList: for albums with several documents, a Python list
of document titles, e.g. track titles
volumelabel: volume label of DVD or CD-ROM
"""
if self.ean and not ean: ean = self.ean
log = logging.getLogger("showDiscMetadata")
#log.debug("rt: %s, albumTitle=%s, title list=%s" \
#% (unicode(rt), albumTitle, unicode(origDocTitleList)))
(packageId, partNo, totalParts) = self.checkForPackagePart()
# log.debug("packageId=%s, partNo=%s, totalParts=%s" % (packageId, partNo, totalParts))
log.debug(ds(locals(), "packageId, partNo, totalParts"))
#log.debug("self.maintype=%s", self.maintype)
if packageId: # Use as default for albumTitle
if not albumTitle or albumTitle.startswith("(Unknown"):
package = product.Product(packageId)
package.readFromDb()
albumTitle = "%s %s %d" % (package.vd["name"], self.ot, partNo)
productDic = {"rt":rt or product.rtDefault(ot), "ot":self.ot,
"ean":ean, "discid":self.discid, "volumelabel":volumelabel,
"packageid":packageId, "name":albumTitle}
# log.debug('productDic["rt"]=%s', productDic["rt"])
productId = self.setProduct(productDic, albumTitle, partNo)
#print db.dicList("SELECT id, ot, rt, name FROM product WHERE id=" + productId)[0]
objectDic = {"ot":self.ot, "descr":productDic["name"], "productid":productId}
objectId = self.setObject(objectDic, productDic["name"])
self.objectInDevice = objectId
if self.maintype and self.maintype == "application":
self.correctCdromEntry(albumTitle)
self.emit(SIGNAL("changedId"),objectId)
#docList = [{"title":t} for t in origDocTitleList]
# docList = []
origList, copyList = [], []
for (i0, t) in enumerate(origDocTitleList):
# print "showDiscMetadata", i0
orig = document.Document(initDic={"title":t, "mimetypeid":mt})
if isrcDic and i0+1 in isrcDic: orig.vd["isxn"] = isrcDic[i0+1]
origList.append(orig)
copyList.append(document.Document(initDic={"title":t}))
#log.info("docList: %s" % uc(docList))
# albumList = [docList if i+1==partNo else [] for i in range(totalParts)]
docsToA = DocsToArchive()
#docs.count = totalParts
docsToA.orig = {partNo:origList} # index -> list of Document()s
docsToA.copy = {partNo:copyList} # index -> list of Document()s
self.fillFields(docsToA=docsToA, objectId=objectId)
def correctCdromEntry(self, albumTitle):
"""If a CD-ROM has exactly one copy, with empty or NULL cp.file,
then it is most likely an incorrect, placeholder entry.
Correct it like this:
cp.file='/'
document: title=object.descr, mimetypeid=130, contype='CD-ROM'
"""
h = self #.hierarchyWidget
# title = albumTitle or "(Unknown)"
if not self.objectId: return
cpDicList = db.dicList("SELECT * FROM cp WHERE objectid=" + self.objectId)
if len(cpDicList) != 1: return
cp = cpDicList[0]
if cp["file"]: return # not empty or NULL
db.execSql("UPDATE cp SET file='/' WHERE id=" + cp["id"])
setTitle = "title=%s," % q(albumTitle) if albumTitle else ''
sql = "UPDATE document SET %s mimetypeid=130, contype='CD-ROM', licenseid=10 WHERE id=%s" \
% (setTitle, cp["documentid"])
db.execSql(sql)
def scheduleSingleAction(self, item, formatId, actionChar):
col = self.formatColumn[formatId]
oldtext = (unicode(getText(item, col)) or '') + " "
item.setText(col, oldtext[:2] + actionChar)
def scheduleDefaultActions(self, docsToA):
"""Schedule default actions, such as making copies of audio tracks in MP3 format,
execScheduledActions().
TODO: Under some criteria, schedule a copy of complete CD in one file
instead of one MP3 file per track,
for example for audio books.
- if the single documents are named tracknn.cdda
- if the album is episode of a series
- if it is an audio book
"""
caption = "scheduleDefaultActions"
log = logging.getLogger("scheduleDefaultActions")
#print "self.dirToImport", self.dirToImport
#print "self.formats", self.formats
#msgbox(caption)
if self.dirToImport: return # Do not schedule default actions when called from importDirectory.
if len(self.formats) == 0: return
# self.defaultFormats is a dictionary maintype=>list of formatIds;
# For the first of these formats, a copy per doc is scheduled by default if it does not exist yet.
defaultFormatId = self.defaultFormats[self.maintype][0] # = {"audio":['57','59']}
defaultFormatCol = self.formatColumn[defaultFormatId]
actionChar = 'N'
# TODO: do not ask if copy exists
msgBox = QMessageBox()
msgBox.setText("Copy whole or parts?")
msgBox.setWindowTitle(caption)
rootButton = msgBox.addButton("Whole", QMessageBox.ActionRole)
partsButton = msgBox.addButton("Parts", QMessageBox.ActionRole)
noneButton = msgBox.addButton("None", QMessageBox.ActionRole)
msgBox.setDefaultButton(rootButton if getLevRatio(docsToA)>0.5 else partsButton)
msgBox.exec_()
if msgBox.clickedButton() == noneButton: return
copyLevel = "album" if msgBox.clickedButton() == rootButton else "document"
c = 0 # counter of scheduled copies
for item in traverseRecursive(self.invisibleRootItem()):
cur = getText(item, self.curColumn)
if cur == ' ': continue
level = getText(item, self.levelColumn)
# For video DVDs and CD-ROMS/DVD-ROMs, schedule copy of root, if necessary.
# print 'level="%s"; self.ot="%s"; self.maintype="%s"' % (level, self.ot, self.maintype)
#if level == "document" \
#or (level == "album" and self.maintype == "application") \
#or (level == "album" and self.ot == "DVD" and self.maintype == "video"):
if level == copyLevel:
s = getText(item, defaultFormatCol) or '0'
try:
existingCopies = int(s)
except ValueError:
existingCopies = 0
# Only schedule copy action if there is no copy yet.
if existingCopies == 0:
self.scheduleSingleAction(item, defaultFormatId, actionChar)
c += 1
return c
def ensureDocumentRecordExists(self):
"""Ensure original (non-archivable) document record exists for current item.
Return its documentId is successful, else None."""
item = self.currentItem()
if not item:
errmsg("ensureDocumentRecordExists: No item")
return None
origDocId = self.getDocId(item)
cnt = getText(item, self.rnColumn)
if origDocId:
return origDocId
#log.info("No origDocId, calling createNewRecord")
albumItem = self.getAlbumItem()
return self.createNewRecord(cnt, item, albumItem) #objectId)
def defaultMimetypeId(self, maintype, level="document"):
if maintype == "audio": return "113"
if maintype == "video":
if level == "album":
return "122" # video/x-dvd
else:
return "112"
return "130" # application/x-iso-image
def createNewRecord(self, rn, dn, item, albumItem):
"""Create new DB entry for document (e.g. audio track) referenced by item (number: cnt),
and cp entry.
Called to execute action M: reMember Metadata
and from createNewCopy() if there is no entry yet.
Return document ID if successful else None.
"""
albumCnt = int((getText(albumItem, self.rnColumn) or '').strip() or 1)
docsToA = self.docsToA if self.dirToImport else None
title = getText(item, self.dbTitleColumn) or getText(item, self.extTitleColumn)
level = getText(item, self.levelColumn)
albumLevel = getText(albumItem, self.levelColumn)
#if level == "album":
## docDic = docsToA[0] if docsToA else None
#docDic = docsToA.getOrig() if docsToA else None
#else:
##docDic = docsToA[int(cnt)-1] if docsToA else None
#docDic = docsToA.getOrig(int(cnt)) if docsToA else None
docDic = docsToA.getOrig(int(rn) if level == "album" else 1) if docsToA else None
albumId = getText(albumItem, self.idColumn)
# print "docDic:", docDic
# print("albumLevel: %s" % albumLevel)
if albumLevel == "album":
objectId = albumId
elif albumLevel == "package":
objectId = dbq.getObjectOfSimpleProduct(albumId)
# print("objectId: %s" % objectId)
# Determine original's mimetype
type = self.defaultMimetypeId(self.maintype, level)
#if self.maintype == "audio":
#type = "113"
#elif self.maintype == "video":
#if level == "album": type = "122" # video/x-dvd
## elif level == "document": type = "112" # MP
#else: type = "112" # MP
#else: type = "130" # application/x-iso-image
# TODO: For imported files from LP or MC directory, put an 'A' or 'B' before name
# see takeTitlesAsFilenames
if level in ("album", "package"):
file = '/'
if self.maintype not in ("audio","video"): type = "111" # CS
elif self.maintype == "audio":
if self.discid:
file = "track%s.cdda" % rn
elif self.dirToImport:
file = docDic["file"] # TODO: split it up
#d = dbdocs[i] if len(dbdocs)>=i+1 else None
#e = docList[i] if len(docList)>=i+1 else None
#file = d["file"] if d and "file" in d else (e["file"] if e else '')
else: file = archive.createFilename(title) #:"part " + unicode(cnt)
if not objectId:
# TODO: Ensure there is an object
errmsg("objectId not given (1987)")
return None
ta = archive.FileDbTransaction("Archiving " + title)
try:
origdoc = document.Document()
origdoc.vd.update({"title":title, "mimetypeid":type, "licenseid":"4"})
ta.sqlList.append(origdoc.save())
docId = origdoc.getId()
ta.sqlList.extend(db.ensureRecordExists('cp',
{'objectid':objectId, 'documentid':docId, 'file':q(file)}))
except Exception as s: # rollback due to SqlError or file error
ta.exceptionHandling(s)
return None
ta.commit() # ta.confirmCommit()
id = objectId if file=='/' else docId
item.setText(self.idColumn, id)
item.setText(self.dbTitleColumn, title)
item.setText(self.fileColumn, file)
return docId
def registerExistingDoc(self): # TODO: reform and reactivate
"""Register existing documents as version of marked document.
"""
log = logging.getLogger("registerExistingDoc")
caption = "Register existing documents"
item = self.currentItem()
origDocId = self.ensureDocumentRecordExists()
# origDocId = h.getDocId(item)
# cnt = getText(item, h.rnColumn)
# if not origDocId:
# log.info("No origDocId, calling createNewRecord")
# self.createNewRecord(cnt, item)
# origDocId = h.getDocId(item)
log.info("origDocId: %s" % origDocId)
title = getText(item, h.dbTitleColumn) or getText(item, h.extTitleColumn)
crit = "title=%s AND id!=%s" % (q(title), origDocId)
f = finddialog.FindEntityAndTypeDialog('document')
#f.helpIntro = "<p>Please select or create a package</p>"
f.critEdit.setText(crit)
f.showCandidates()
if not f.exec_(): return
# sql = "INSERT INTO rel (ent1,id1,relid,ent2,id2) VALUES (5,%s,19,5,%s)" % (f.selectedId(), origDocId)
sql = "UPDATE document SET versionof=%s WHERE id=%s" % (origDocId, f.selectedId())
log.debug(sql)
db.execSql(sql)
def addMetadata(self):
"""Register metadata for product and document.
Currently handled are the following data:
- persons as actors, directors, composers, scriptwriters
- date (document.date and product.releasedate)
- rating in document.summary
- producer (label) as product.companyid
- product.ean
- document.review from metadata fields summary, summary2, summary3
- document.languageid
- document.length
"""
caption = "addMetadata"
log = logging.getLogger("addMetadata")
item = self.currentItem()
product = self.getProductWithEan(item)
if not product:
errmsg("No product with EAN", caption)
return
# Find the infosourcecontent entry of this product.
# TODO: prepend EAN with 0 if needed
# adapt to new FindDialog syntax
f = finddialog.FindEntityAndTypeDialog("infosourcecontent")
title = product["name"]
crit = "mainfield=%s" % q(title)
ean = product["ean"].strip()
if ean: crit += " OR publicid=%s" % q(ean)
f.critEdit.setText(crit)
f.showCandidates()
if not f.exec_(): return
id = f.selectedId()
mdr = metadatareader.MetadataReader(infosourcecontentid=id)
if not mdr.exec_(): return
origDocId = self.ensureDocumentRecordExists()
#log.info("origDocId: %s" % origDocId)
# Let user confirm the document.
f = finddialog.FindEntityAndTypeDialog("document",
msg = "Please select the document these metadata refer to.")
f.critEdit.setText("#%s" % origDocId)
f.showCandidates()
if not f.exec_(): return
docId = f.selectedId()
# Let user confirm the product.
f = finddialog.FindEntityAndTypeDialog("product",
msg = "Please select the product these metadata refer to.")
pid = product["id"]
if pid: f.critEdit.setText("#%s" % pid)
f.showCandidates()
if not f.exec_(): return
productId = f.selectedId()
log.info("docId: %s; productId: %s; registering roles (actors etc.)" % (docId, productId))
# TODO: Ask if data refer to this document or a prototype (original).
# TODO: If a person (actor etc.) is created, guess language from that of document.
getVal = metadatareader.getAttributeValue # Give function a handier name.
sep = getVal(mdr.attributes, "sep")
if not document.registerRoles(getVal(mdr.attributes, "actors"), sep, docId, 'actors'): return
if not document.registerRoles(getVal(mdr.attributes, "directors"), sep, docId, 'directors'): return
if not document.registerRoles(getVal(mdr.attributes, "composers"), sep, docId, 'composers'): return
if not document.registerRoles(getVal(mdr.attributes, "scriptwriters"), sep, docId, 'scriptwriters'): return
log.debug("registering date etc.")
rawdate = getVal(mdr.attributes, "date")
normdate = db.getDate(rawdate)
if normdate:
sql = "UPDATE document SET date='%s' WHERE id=%s" % (normdate, docId)
db.execSql(sql)
sql = "UPDATE product SET releasedate='%s' WHERE id=%s" % (normdate, productId)
db.execSql(sql)
rating = getVal(mdr.attributes, "rating")
sql = "UPDATE document SET summary=%s || COALESCE(summary, '') WHERE id=%s" % (q(rating), docId)
db.execSql(sql)
# getMainImage getVal(mdr.attributes, "imagepage")
producerText = getVal(mdr.attributes, "producer")
if producerText:
f = finddialog.FindEntityAndTypeDialog("company")
f.helpIntro = "Please select a company as producer."
f.critEdit.setText("name~*%s" % q(producerText))
f.newRecordValue = producerText
f.initDic = {"remark":"producer of %s" % title}
f.showCandidates()
if f.exec_() and f.selectedId():
sql = "UPDATE product SET companyid=%s WHERE id=%s" % (f.selectedId(), productId)
db.execSql(sql)
ean = getVal(mdr.attributes, "ean")
if ean:
log.debug("registering EAN: %s" % ean)
sql = "UPDATE product SET ean=%s WHERE id=%s" % (q(ean), productId)
db.execSql(sql)
genre = getVal(mdr.attributes, "genre")
if genre:
sql = "UPDATE document SET contype=%s WHERE id=%s" % (q(genre), docId)
db.execSql(sql)
text = getVal(mdr.attributes, "summary")
if text:
sql = "UPDATE document SET review=%s WHERE id=%s" % (q(text), docId)
db.execSql(sql)
text = getVal(mdr.attributes, "languages")
if text:
llist = db.parseLanguages(text)
if llist:
sql = "UPDATE document SET languageid=%s WHERE id=%s" % (llist[0], docId)
db.execSql(sql)
text = getVal(mdr.attributes, "length")
if text:
sql = "UPDATE document SET length='%s' WHERE id=%s" % (getLength(text), docId)
log.debug("registering length: %s" % sql)
db.execSql(sql)
url = getVal(mdr.attributes, "image")
if url:
# Download image, create document for it, create rel
log.debug("registering image from URL: %s" % url)
document.downloadImage(url, productId)
self.fillFields(productId=product["id"])
#self.productWidget.reread()
#self.docWidget.documentWidget.reread()
#log.debug("Done.")
def fetchDataByEan(self):
caption = "fetchDataByEan"
# log = logging.getLogger("fetchDataByEan")
item = self.currentItem()
product = self.getProductWithEan(item)
if not product:
errmsg("No product with EAN", caption)
return
ean = product["ean"]
if not ean: return
#msgbox("Fetching data for EAN: " + ean)
#print("i = infosource.Infosource(100)")
i = infosource.Infosource(100)
p = i.getPageByEan(ean)
idList = i.recordDetail(p,'product','ean')
msgbox("Ready fetching data for EAN: " + ean)
def fetchDataByTitle(self):
albumItem = self.getAlbumItem()
if albumItem:
title = getText(albumItem, self.dbTitleColumn)
else:
(text,ok) = QInputDialog.getText(self, "Enter title",
"Please enter title here.")
if not ok: return
title = unicode(text)
msgbox("Fetching data for title: " + title)
i = infosource.Infosource(128)
p = i.getPageByTitle(title,'any')
idList = i.recordDetail(p,'product','ean')
if not idList: msgbox("No data found"); return
crit = ' OR '.join(["id=%s" % id for id in idList])
f = finddialog.FindDialog("infosourcecontent")
f.critEdit.setText(crit)
if not f.exec_(): return
id = f.selectedId()
mdr = metadatareader.MetadataReader(infosourcecontentid=id)
if not mdr.exec_(): return
if not productId: return
p = product.Product()
p.readFromDb(productId)
p.getMetadataValues(mdr.attributes)
ta = archive.FileDbTransaction('') #caption)
try:
# DB and file operations
ta.sqlList = p.saveExtended() # calls doc.save() and gets id; returns SQL list
for relSql in p.relSqls:
ta.sqlList.append(db.execSql(relSql % p.vd["id"]))
except Exception as s: # rollback due to SqlError or file error
ta.exceptionHandling(s)
# else:
# if ta.confirmCommit():
# self.setId(product.vd["id"])
# Helper functions ==============================================
# def productDiscId(self):
# """Return discid from product DB record or None."""
# self.productWidget.getValue("discid")
def exportCopy(self, rn, dn, item, level, origDocId, title, filename,
destPath, destFormat, albumItem, destDir, multiProgress):
caption = "exportCopy"
log = logging.getLogger(caption)
log.debug("rn=%s, item=%s, destFormat=%s, albumItem=%s", rn, item, destFormat, albumItem)
docsToA = self.docsToA
if not self.setDevice(): return
src = None # path if source is file or directory
(documentId, path) = document.getDocumentVersion(origDocId)
log.debug("version documentId: %s; path: %s" % (documentId, path))
path = path or document.getAvailableFile(documentId)
if not path: return
(dirName, fileName) = os.path.split(path)
destPath = destDir + '/' + fileName
cmd = ["cp", path, destPath]
statinfo = os.stat(path)
endValue = statinfo.st_size
p = DestinationFileProcess(multiProgress, "Export " + fileName,
cmd, destPath, endValue)
p.startCmd()
print "after startCmd()"
#if e:
#errmsg(e) #, caption)
#return False
return True
# TODO:
# - How to calculate the uncompressed size? Is the compression factor known?
#def createNewCopy(self, rn, item, destFormat, albumItem, multiProgress):
def createNewCopy(self, rn, dn, item, level, origDocId, title, filename, destPath,
destFormat, albumItem, albumObjectId, albumTitle, albumDescr, albumCnt,
multiProgress):
"""Create new copy of document (e.g. audio track) referenced by item (number: cnt), in given format.
Not for registering existing documents.
albumItem is the item itself if it is an album, or
the parent (ancestor) album, if the item is a document.
Needed to find out which object a document belongs to,
even if the document is not yet in the DB.
Strictly speaking, this does several jobs:
- Ripping and converting is delegated to ripEncode()
- Copying/moving to destination
- Regrouping
One or more of these may be null.
class FileProcess
destPath
destSize estimate
progress
hash (copied from src or calculated)
size (of destPath)
origSize
compFactor
function can be:
dir -> isoFile
cp or mv file
rip file
dvdbackup to dir and then to isoFile
dd
mp3[un]wrap
tiffcp %s '%s'; tiff2pdf
convert
cdparanoia
genisoimage
parameters:
destPath, srcList, device
"""
#def ripEncode(self, rn, item, level, origDocId, title, filename, destPath,
#destFormat, albumItem, albumObjectId, albumTitle, albumDescr, albumCnt,
#multiProgress):
caption = "createNewCopy"
log = logging.getLogger(caption)
# docsToA = self.docsToA
# if not self.setDevice(): return
if not origDocId: # No original document record yet.
self.createNewRecord(rn, dn, item, albumItem)
origDocId = self.getDocId(item)
#origSize = None # size in bytes of original, e.g. audio track
#compFactor = 1 # typical compression factor of destFormat
#src = None # path if source is file or directory
log.debug(ds(locals(), "albumObjectId,albumCnt,albumTitle"))
(origDoc, copyDoc) = self.docsToA.getAlbumDocPair(albumCnt, level, dn)
log.debug(ds(locals(), "origDoc,copyDoc"))
# copyDoc is a single document
#if not copyDoc: # TODO
#errmsg("Nothing to copy")
#return
# if docDic and "copymtid" in docDic: srcFormat = mimetype.Mimetype(docDic["copymtid"])
hashSql = None
if self.discid:
success = self.ripEncode(rn, dn, item, level, origDocId, title, filename,
destPath, destFormat,
albumItem, albumObjectId, albumTitle, albumDescr, albumCnt,
multiProgress)
if not success: return False
self.scheduleSingleAction(item, destFormat.getId(), ' ') # remove action from schedule
# TODO: Show that new copy exists.
caption = "New Document: " + title
# Now file is in toarchive, so there is no precalc hash in rawfilelist!!!
doc = document.Document(); doc.src = destPath
if doc.checkDuplicate(): return
doc.vd.update({"title":title, "mimetypeid":destFormat.getId()}) #, "prototypic":"false"
doc.vd.update({"versionof":origDocId})
id = document.createDocumentRecord(doc, caption, confirm=False)
return True # success
if copyDoc and "mimetypeid" in copyDoc.vd:
srcFormat = mimetype.Mimetype(copyDoc.vd["mimetypeid"])
if self.dirToImport:
srcPath = copyDoc.src #docDic["albumDir"] + '/' + docDic["file"]
origSize = os.path.getsize(srcPath)
files = [d.src for d in docsToA.getCopy()]
exts = set([os.path.splitext(f)[1].lower() for f in files])
filetext = getText(item, self.fileColumn)
if self.dirToImport and (level == "document" or filetext == '/'):
if srcFormat.getId() == destFormat.getId():
cmd = "mv %s %s" % (qs(srcPath), qs(destPath)) # better use copyRename()? TODO
shell = True
# Copy precalc hash. TODO: Check if isodate is correct, see archive.copyAndRecord()
hashSql = textwrap.dedent("""
INSERT INTO rawfilelist (host, file, size, isodate, hash)
SELECT host, %s, size, isodate, hash
FROM rawfilelist
WHERE file=%s
""")[1:-1] % (q(os.path.realpath(destPath)), q(os.path.realpath(srcPath)))
print "hashSql", hashSql
elif self.dirToImport and level == "album" and exts == set([".mp3"]) and destFormat.getId() == '57':
srcList = ''; origSize = 0
for docDic in docList:
srcPath = docDic["albumDir"] + '/' + docDic["file"]
srcList += "'%s' " % srcPath
origSize += os.path.getsize(srcPath)
if len(docList) > 1:
cmd = "mp3wrap '%s' %s" % (destPath, srcList)
else:
cmd = "cp %s '%s'" % (srcList, destPath)
elif self.maintype == "text":
if not self.dirToImport or level != "album":
errmsg("Not yet supported")
return
if exts == set([".tif"]):
tmpfile = archive.getFreeToarchiveFilepath("tmpfile")
srcList = ''; origSize = 0
for docDic in docList:
srcPath = docDic["albumDir"] + '/' + docDic["file"]
srcList += "'%s' " % srcPath
origSize += os.path.getsize(srcPath)
if not srcList:
errmsg("No source file")
return
cmd1 = "tiffcp %s '%s'; tiff2pdf '%s'; rm '%s'" % (srcList, tmpfile, tmpfile, tmpfile)
cmd = "(%s) > '%s'" % (cmd1, destPath)
elif exts == set([".jpg"]):
tmpfile = archive.getFreeToarchiveFilepath("tmpfile")
srcList = ''; origSize = 0
for docDic in docList:
srcPath = docDic["albumDir"] + '/' + docDic["file"]
srcList += "'%s' " % srcPath
origSize += os.path.getsize(srcPath)
if not srcList:
errmsg("No source file")
return
cmd = "convert -monitor -verbose %s '%s'" % (srcList, destPath)
print("cmd:", cmd)
# return
elif self.maintype == "audio":
# if self.discid: archiving a CD/DVD
# if self.dirToImport: importing a directory
if self.discid:
if self.objectInDevice == albumObjectId:
# cmd1 is the command that reads (or rips) the file and writes it to stdout
# cmd2 is the command that reads the file from stdin and writes it in the destination format to dest
if level == "album":
cmd1 = "cdparanoia -Z -d %s 1- -" % (self.device)
origSize = getTrackSize(self.device, None)
print("origSize ", origSize)
elif level == "document":
# Without -Z, with bad CDs, it hangs forever.
cmd1 = "cdparanoia -Z -d %s %s -" % (self.device, rn)
origSize = getTrackSize(self.device, rn)
print("origSize ", origSize)
else:
msg = "Need %s in drive %s. Currently in drive is\n" % (albumDescr, self.device)
msg += db.descrWithId("object", self.objectInDevice) if self.objectInDevice else "no audio CD"
errmsg(msg)
return
elif self.dirToImport:
print("rn", rn)
if level == "album": # Convert to wav and concatenate; rn is album counter
origSize = 0 #getTrackSize(self.device, None)
#docList = self.albumList[int(rn)-1]
# This first converts to wav, then concatenates and finally
# in cmd2, converts to destination format.
# Other strategies are possible, e.g. if source is MP3:
# just use mp3wrap.
# Another strategy, for TIF->PDF:
# tiffcp, then tiff2pdf
# Need intermediate file for that, or named pipe; also for mplayer
# one command is a list of strings; for args: "%(srcPath)"
# there may be several commands
# there may be tmp.files, as many as src files
# is there an intermediate format (such as wav)?
# To summarize data per action:
# - list of commands, with placeholders for src and dest filenames
# - compression factor for progress calculation
# - how to detect errors (from exitcode or stderr)
# TODO: Murks hier, gründlich überarbeiten!
cmd1 = "("
for docDic in docList:
#docDic["albumDir"] = albumDir
srcPath = docDic["albumDir"] + '/' + docDic["file"]
print(srcPath)
origSize += os.path.getsize(srcPath)
cmd1 += "ffmpeg -i %s -f wav -; " % qs(srcPath) #(docDic["albumDir"] + '/' + docDic["file"])
cmd1 += ")"
elif level == "document": # Convert to wav; here, rn is file counter
docDic = self.albumList[albumCnt-1][int(rn)-1]
srcPath = docDic["albumDir"] + '/' + docDic["file"]
cmd1 = "ffmpeg -i '%s' -f wav -" % srcPath
origSize = os.path.getsize(srcPath)
origSize = origSize * 10 # compFactor of e.g. WMA TODO
print("origSize ", origSize)
print(cmd1)
else:
descs = getDescendantItems(h, item)
msg = ''
for i in descs:
msg += getItemDescr(h, i) + '\n'
msgbox(msg)
# msgbox("Creating new documents from archived ones is not yet supported", caption)
return
if destFormat.subtype() == "mpeg":
cmd2 = "lame - '%s'"
compFactor = 10
elif destFormat.subtype() == "x-ogg":
cmd2 = "oggenc - -o '%s'"
compFactor = 10
elif destFormat.subtype() == "x-aac":
cmd2 = "faac - -o '%s'"
compFactor = 10
elif destFormat.subtype() == "flac":
cmd2 = "flac - -o '%s'"
compFactor = 10
elif destFormat.subtype() == "x-ms-wma":
cmd2 = "ffmpeg -i - -acodec wmav2 -ab 128 '%s'"
compFactor = 10
pass # Conv. WMA to MP3: mplayer -vo null -vc dummy -af resample=44100 -ao pcm -waveheader $i && lame -m s audiodump.wav -o $i
# mplayer src.wma -ao pcm -ao pcm:file=file.wav
#cmd2 = "flac - -o '%s'"
#compFactor = 10
else:
errmsg("Audio format not yet supported: " + destFormat.subtype())
return
cmd2 = cmd2 % destPath
cmd = cmd1 + " | " + cmd2
shell = True
elif self.maintype == "video":
# If album is video DVD copy (i.e. a DVD playable in standalone player,
# but not original, so without CSS), then creating an ISO from the DVD,
# the same way as with (game etc.) CD-ROMs, is one method of creating a copy,
# maybe even the preferred one. To determine this, take into account:
# format.subtype()? origDoc etc.
print "self.discid=%s self.objectInDevice=%s self.objectId=%s" % (self.discid,self.objectInDevice,self.objectId)
if self.discid and self.objectInDevice == self.objectId:
if destFormat.subtype() == "mp4":
print "d = dvdinfo.HbDvd(self.device)"
d = dvdinfo.HbDvd(self.device)
print "discTitle = d.getDiscTitle()"
discTitle = d.getDiscTitle()
print "discTitle ", discTitle
ix = d.getLongestTitleIndex()
# print "getLongestTitleIndex", d.getLongestTitleIndex()
#ix = d.getTitleIndex(t)
print "getLongestTitleIndex ix", ix
#rawdest = "%s/%s.mp4" % (outputdir, discTitle)
dest = destPath #uniqueFileName(rawdest)
print "dest ", dest
# al = d.getAudio(title=t)
# cmd = "HandBrakeCLI -i /dev/sr0 -o /unibas_archive/raw/DVD/Ferkeltest.mp4 %s %s" % (opts_a, opts_s)
cmd = ["HandBrakeCLI", "-i", self.device]
cmd.extend(["-t", "%d" % ix])
cmd.extend(["-o", dest])
cmd.extend(d.getAudioOption())
cmd.extend(d.getSubtitleOption())
shell = False
outRegExpString = ".*Encoding: task \d+ of \d+, (\d+)\.\d+ %.*"
errRegExpString = "Encoding: task \d+ of \d+, (\d+)\.\d+ %"
print ' '.join(cmd)
# msgbox(' '.join(cmd))
# retcode = subprocess.call(cmd)
# return #(retcode, dest)
elif QMessageBox.question(None, caption, "Copy DVD dir structure with dvdbackup?",
QMessageBox.StandardButtons(QMessageBox.No | QMessageBox.Yes),
QMessageBox.No) == QMessageBox.Yes:
# Note: dvdbackup produces a directory; create iso file with:
# genisoimage -gui -dvd-video -o "%s" "%s"' % (dest, src)
cmd = "dvdbackup -M -i %s -o '%s'" % (self.device, destPath)
origSize = self.dvdSize if self.dvdSize else 8123456789
# not so simple; must be packed as ISO, errors must be detected
# even with -v option, there is no progress information
# but we can first list with -L all files and their sizes,
# then watch the copy files as they are created,
# calculate the total size of the copies and use this as progress indicator.
elif QMessageBox.question(None, caption, "Create ISO9660 image with dd?",
QMessageBox.StandardButtons(QMessageBox.No | QMessageBox.Yes),
QMessageBox.Yes) == QMessageBox.Yes:
cmd = "dd if='%s' of='%s'" % (self.device, destPath)
else:
msgbox("Currently no other format supported!")
# TODO dvdbackup
return
#elif self.filesToArchive:
elif self.dirToImport:
#msgbox("srcFormat.getId(): %s" % srcFormat.getId())
#if srcFormat.getId() == "125": # video DVD ISO
#cmd = "cp '%s' '%s'" % (srcPath, destPath)
if srcFormat.getId() == "122": # physical video DVD content
# eigentlich sehr unsauber; destFormat ist nur bekannt von sched. Action
# eigentlich könnte man aus 122 auch andere Formate produzieren
# ist also nur dann korrekt wenn ich hier 125 selektiere
#upcaseDvdDir(src)
#cmd = '/usr/bin/genisoimage -gui -dvd-video -o "%s" "%s"' % (dest, src)
#print "orig_src", origDoc.vd["src"]
#print "copy_src", copyDoc.vd["src"]
src = copyDoc.src #docDic["albumDir"]
#msgbox(src)
upcaseDvdDir(src)
# origSize = os.path.getsize(srcPath)
origSize = getDirSize(src); compFactor = 1
cmd = "/usr/bin/genisoimage -gui -dvd-video -o %s %s" \
% (qs(destPath), qs(src))
elif srcFormat.getId() in ("104","108","125","168","133"): # .avi or .ogv file or so
#cmd = "cp '%s' '%s'" % (srcPath, destPath)
cmd = "mv %s %s" % (qs(srcPath), qs(destPath)) # better use copyRename()?
# Copy precalc hash. TODO: Check if isodate is correct, see archive.copyAndRecord()
hashSql = textwrap.dedent("""
INSERT INTO rawfilelist (host, file, size, isodate, hash)
SELECT host, %s, size, isodate, hash
FROM rawfilelist
WHERE file=%s
""")[1:-1] % (q(os.path.realpath(destPath)), q(os.path.realpath(srcPath)))
print "hashSql", hashSql
#msgbox(hashSql)
else:
errmsg("Format not yet supported: %s" % db.descrWithId("mimetype",srcFormat.getId()))
return
else:
msg = "Wrong or no medium in device"
errmsg(msg, caption)
return
elif self.maintype == "application":
compFactor = 1
print("self.discid=%s self.objectInDevice=%s self.objectId=%s" % (self.discid,self.objectInDevice,self.objectId))
print "self.ot", self.ot
if self.ot == "CD":
origSize = 700000000
elif self.ot == "DVD":
origSize = 4700000000
if self.discid and self.objectInDevice == self.objectId:
# cmd = "dd if='%s' of='%s'" % (self.device, destPath)
cmd = ["dd", u"if={}".format(self.device), u"of={}".format(destPath)]
# shell = False # That's the default.
# Sometimes I get an error message here, don't know why.
# The output seems to be good, can be mounted, nothing missing.
#dd: reading `/dev/scd0': Input/output error
#1098976+0 records in
#1098976+0 records out
#562675712 bytes (563 MB ) copied, 140.734 s, 4.0 MB/s
#ProcessError: 5
else:
msg = "Need shown CD in drive %s. Currently in drive is\n" % self.device
msg += db.descrWithId("object", self.objectInDevice) if self.objectInDevice else "apparently no CD-ROM"
errmsg(msg)
return
else:
msgbox("Not yet implemented format copy: " + self.maintype)
return
print(cmd)
# TODO: progress feedback and interrupt possibility (use dd_rescue?)
# TODO: copy directly to destination? (not necessary if on same filesystem)
# TODO: create md5sum at the same time?
# TODO: Use startShellCmd
if hashSql:
#msgbox("now executing hashSql")
db.execSql(hashSql)
try:
# res = btoc(cmd)
# shellCmd = 'sh -c "%s"' % cmd
# print(shellCmd)
if origSize and compFactor:
endValue = origSize / compFactor
else:
endValue = 100 #5000000
# print "endValue ",endValue
#pp = ProcessProgress(shellCmd, windowTitle="Archiving file " + rn,
#labelText=destPath, endValue=endValue)
#pp.sizeFile = destPath
#pp.start()
#print "pp.success", pp.success
#if not pp.success:
#return False
# m.startShellCmd(shellCmd, destPath, endValue)
multiProgress.outRegExpString = outRegExpString
multiProgress.errRegExpString = errRegExpString
print "before startCmd"
multiProgress.startCmd(cmd, destPath, endValue, shell=shell)
print "after startCmd"
if not multiProgress.success:
return False
except Exception as s:
errmsg(unicode(s), caption)
return False
# Copy successful. TODO: Copy precalc hash from rawfilelist to filelist.
# Copy precalc hash is done in document.saveExtended()?
#If it was from file or dir, delete that. TODO: test and activate
#if self.filesToArchive: del self.filesToArchive[origDocId]
# Deletion must be done in a separate step.
# The user might want to take several different copies of the same source.
#if src:
#askAndRemove(src, "Delete source",
#"Copy succeeded, so source is no longer needed:\n%s\nDelete it?" % src)
self.scheduleSingleAction(item, destFormat.getId(), ' ') # remove action from schedule
# TODO: Show that new copy exists.
caption = "New Document: " + title
# Now file is in toarchive, so there is no precalc hash in rawfilelist!!!
doc = document.Document(); doc.src = destPath
if doc.checkDuplicate(): return
doc.vd.update({"title":title, "mimetypeid":destFormat.getId()}) #, "prototypic":"false"
doc.vd.update({"versionof":origDocId})
id = document.createDocumentRecord(doc, caption, confirm=False)
return True # success
# db.execSql("INSERT INTO rel (ent1,id1,relid,ent2,id2) VALUES (5,%s,19,5,%s)" % (id,origDocId))
#sh -c "(ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/01 Track 1.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/02 Track 2.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/03 Track 3.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/04 Track 4.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/05 Track 5.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/06 Track 6.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/07 Track 7.wma' -f wav -; ffmpeg -i '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/08 Track 8.wma' -f wav -; ) | lame - '/unibas_archive/raw/CD/ean9783866040649 Pu der Baer 3 CDs/CD 1/08 Track 8.wma'"
# TODO:
# - How to calculate the uncompressed size? Is the compression factor known?
def ripEncode(self, rn, dn, item, level, origDocId, title, filename, destPath,
destFormat, albumItem, albumObjectId, albumTitle, albumDescr, albumCnt,
multiProgress):
"""Create new copy of document in toarchive. Make no DB entries.
Maybe use for createNewCopy?
"""
# not needed: item, oridDocId, albumItem, albumObjectId, albumTitle, albumDescr, albumCnt
caption = "ripEncode"
log = logging.getLogger(caption)
if not self.setDevice(): return False
log.debug("self.maintype=%s", self.maintype)
if self.maintype == "audio":
cmd1data = {"album":('1-',''), "document":(dn,dn)}.get(level)
if not cmd1data:
errmsg("Ripping audio on level %s not yet supported." % level)
return False
cmd1 = "cdparanoia -Z -d %s %s -" % (self.device, cmd1data[0])
origSize = getTrackSize(self.device, cmd1data[1])
cmd2Dic = {"mpeg":("lame - '%s'",15), "x-ogg":("oggenc - -o '%s'",14),
"x-aac":("faac - -o '%s'",10), "flac":("flac - -o '%s'",2)}
cmd2data = cmd2Dic.get(destFormat.subtype())
if not cmd2data:
errmsg("Audio format not yet supported: " + destFormat.subtype())
return False
(cmd2, compFactor) = cmd2data
cmd = cmd1 + " | " + cmd2 % destPath
labelText = destPath.split('/')[-1]
p = DestinationFileProcess(multiProgress, labelText,
cmd, destPath, origSize/compFactor, shell=True)
elif self.maintype == "video":
# If album is video DVD copy (i.e. a DVD playable in standalone player,
# but not original, so without CSS), then creating an ISO from the DVD,
# the same way as with (game etc.) CD-ROMs, is one method of creating a copy,
# maybe even the preferred one. To determine this, take into account:
# format.subtype()? origDoc etc.
if self.discid and self.objectInDevice == self.objectId:
if destFormat.subtype() == "mp4":
d = dvdinfo.HbDvd(self.device)
discTitle = d.getDiscTitle()
ix = d.getLongestTitleIndex()
cmd = ["HandBrakeCLI", "-i", self.device]
cmd.extend(["-t", "%d" % ix, "-o", destPath])
cmd.extend(d.getAudioOption())
cmd.extend(d.getSubtitleOption())
outRegExpString = ".*Encoding: task \d+ of \d+, (\d+)\.\d+ %.*"
errRegExpString = "Encoding: task \d+ of \d+, (\d+)\.\d+ %"
print ' '.join(cmd)
p = PercentageProcess(multiProgress, "labelText", cmd,
outRegExpString=outRegExpString)
# msgbox(' '.join(cmd))
# retcode = subprocess.call(cmd)
# return #(retcode, dest)
elif self.maintype == "application":
origSize = {'CD':700000000, 'DVD':4700000000}.get(self.ot) or 4700000000
if self.discid and self.objectInDevice == self.objectId:
cmd = ["dd", u"if={}".format(self.device), u"of={}".format(destPath)]
p = DestinationFileProcess(multiProgress, "RipEncode " + destPath,
cmd, destPath, origSize) # shell=False by default
else:
msgbox("Not yet implemented format copy: " + self.maintype)
return False
res = p.startCmd()
if res:
errmsg(res, caption)
return False
return True
class AlbumAction():
"""Experimental.
3 cases:
1. From disc:
- CD: we know only titles
- DVD: propose dvdbackup, warn that size is big,
ask if full DVD or main feature (default full),
propose alternative DVDShrink of k9copy;
album with root doc only
2. From directory
We know things from dir and file names
3. From archive
We have data in DB already.
TODO: Test mp3wrap!
"""
def __init__(self, h, item, format, actionChar):
log = logging.getLogger("initAlbumAction")
#log.debug("rn=%s, format=%s, self.albumList=%s" % (rn, unicode(format), unicode(self.albumList)))
#caption = "createNewCopy"
# print "createNewCopy(rn, item, format):", rn, item, format
self.level = getText(item, h.levelColumn)
self.origDocId = h.getDocId(item)
title = getText(item, h.dbTitleColumn) or getText(item, h.extTitleColumn)
self.albumItem = h.getAncestorAlbumItem(item)
self.albumObjectId = getText(self.albumItem, h.idColumn) if self.albumItem else None
self.albumCnt = int(getText(self.albumItem, h.rnColumn)) if self.albumItem else None
docList = h.albumList[self.albumCnt-1] if h.albumList and self.albumCnt else None
#log.debug("albumCnt=%s; docList=%s; h.maintype=%s" % (self.albumCnt, unicode(docList), unicode(h.maintype)))
#if level=="album":
#self.docDic = docList[0] if docList else None
#else:
#self.docDic = docList[int(rn)-1] if docList else None
self.docDic = docList[0 if level=="album" else int(rn)-1] if docList else None
log.debug("docDic=%s" % docDic)
if self.docDic and "copymtid" in self.docDic:
self.srcFormat = mimetype.Mimetype(self.docDic["copymtid"])
if h.dirToImport:
self.srcPath = self.docDic["albumDir"] + '/' + self.docDic["file"]
origSize = os.path.getsize(self.srcPath)
exts = set([os.path.splitext(docDic["file"])[1].lower() for docDic in self.docList])
if h.dirToImport and self.level == "document":
if self.srcFormat.getId() == format.getId():
cmd = "cp '%s' '%s'" % (srcPath, destPath)
elif h.dirToImport and level == "album" and exts == set([".mp3"]) and format.getId() == '57':
srcList = ''; origSize = 0
for docDic in docList:
srcPath = docDic["albumDir"] + '/' + docDic["file"]
srcList += "'%s' " % srcPath
origSize += os.path.getsize(srcPath)
if len(docList) > 1:
cmd = "mp3wrap '%s' %s" % (destPath, srcList)
else:
cmd = "cp %s '%s'" % (srcList, destPath)
elif h.maintype == "text":
pass
class AlbumForm(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self,parent)
self.setWindowTitle("Album Form")
self.setMinimumWidth(800)
self.topProductWidget = product.ProductWidget('0','0')
self.mainSplitter = QSplitter(Qt.Vertical, self)
self.mainSplitter.addWidget(self.topProductWidget )
self.tabWidget = QTabWidget(self) #, "tabWidget")
self.mainSplitter.addWidget(self.tabWidget)
self.setCentralWidget(self.mainSplitter)
self.contentsPage = QWidget() #self.tabWidget) #, "Main Page")
self.contentsLayout = QHBoxLayout()
h = self.hierarchyWidget = AlbumHierarchy(self.contentsPage)
h.form = self
h.objectId = None
h.copydir = None
h.maintype = None # "audio", "video" etc. for CD/DVD in drive
h.formats = []
h.formatColumn = {} # keys: mimetype IDs; values: column numbers
h.setSelectionMode(QAbstractItemView.ExtendedSelection)
h.defineBaseColumns()
h.setHeaderLabels(h.baseColumnNames) #["Level","Title"])
h.setColumnCount(h.columnCount)
self.contentsLayout.addWidget(h)
self.contentsPage.setLayout(self.contentsLayout)
self.tabWidget.addTab(self.contentsPage, "Contents")
self.productWidget = product.ProductWidget(None,'5') #None, "auto")
self.tabWidget.addTab(self.productWidget, "Product")
# self.objectWidget = object.ObjectWidget(None,'auto')
self.objectWidget = object.ObjectWithOwnerWidget()
self.tabWidget.addTab(self.objectWidget, "Object")
self.docWidget = document.DocumentWithRolesWidget()
self.tabWidget.addTab(self.docWidget, "Document")
#self.connect(self.objectWidget, SIGNAL("changedId"), self.setId)
self.connect(self.objectWidget.objectWidget, SIGNAL("changedProductId"), self.productWidget.setId)
self.connect(self.objectWidget.objectWidget, SIGNAL("changedProductId"), self.topProductWidget.setId)
self.connect(self.productWidget, SIGNAL("changedId"), self.topProductWidget.setId)
self.connect(self.hierarchyWidget, SIGNAL("changedDocId"), self.changedDocId)
self.connect(self.hierarchyWidget, SIGNAL("changedObjectId"), self.changedObjectId)
self.connect(self.hierarchyWidget, SIGNAL("changedProductId"), self.productWidget.setId)
self.setMenu()
#self.productWidget.setId(1)
self.docWidget.setId(None)
def changedDocId(self, id):
# if not id: return
self.docWidget.setId(id)
def changedObjectId(self, id):
self.objectWidget.setId(id)
def setMenu(self):
h = self.hierarchyWidget
fileMenu = self.menuBar().addMenu("&File")
fileMenu.addAction("Select medium", self.selectMedium)
fileMenu.addAction("Select medium via mediumid", self.selectMediumViaMediaid)
fileMenu.addAction("Select medium via product", self.selectProduct)
fileMenu.addAction("Select series", self.selectSeries)
fileMenu.addAction("Select medium via document", self.selectDocument)
#fileMenu.addAction("Import directory (&old)", self.importDirectory_old)
fileMenu.addAction("Import &directory", h.importDirectoryDialog)
fileMenu.addAction("Import &file album", h.importFileAlbum)
fileMenu.addAction("Reproduce album", h.reproduceAlbum)
#fileMenu.addAction("Commit import", self.commitImport)
fileMenu.addAction("E&xecute scheduled actions", self.execScheduledActions)
fileMenu.addAction("Check &barcode", h.checkBarcode)
fileMenu.addAction("&Check disc", h.checkDisc)
fileMenu.addAction("Check &audio CD", h.checkAudioCd)
fileMenu.addAction("Check &video DVD", h.checkVideoDvd)
fileMenu.addAction("addVideoDvd", h.addVideoDvd)
#fileMenu.addAction("Archive new video DVD directory", self.archiveDvdDir)
fileMenu.addAction("Prepare (tag) album", h.prepareAlbum)
fileMenu.addAction("createIsoFile", h.createIsoFile)
#fileMenu.addAction("Copy album to archive", self.copyAlbumToArchive)
#fileMenu.addAction("Create album copy on media", self.createAlbumCopy)
fileMenu.addAction("createMp3wrap", h.createMp3wrap)
fileMenu.addAction("Add &package", self.addPackage)
fileMenu.addAction("Open series", self.openSeries)
fileMenu.addAction("Close series", self.closeSeries)
fileMenu.addAction("&Quit", self, SLOT("close()")) #, CTRL+Key_Q )
fileMenu.addAction("&Save", self.save)
fileMenu.addAction("archivePartDoc", self.archivePartDoc)
editMenu = self.menuBar().addMenu("&Edit")
editMenu.addAction("Schedule action", self.scheduleAction)
editMenu.addAction("Take over external document titles", self.takeTitles)
editMenu.addAction("Take titles als file names", self.takeTitlesAsFilenames)
editMenu.addAction("Create &work", self.createWork)
editMenu.addAction("Adopt documents from product", self.adoptDocuments)
#editMenu.addAction("Register existing document as version", h.registerExistingDoc)
editMenu.addAction("Make document an episode of a series", self.makeEpisode)
editMenu.addAction("Remove episode from series", self.removeEpisode)
editMenu.addAction("Get external titles from FreeDB", self.getFreeDbTitles)
editMenu.addAction("Get external titles from file", self.getTitlesFromFile)
editMenu.addAction("Set owner", self.setOwner)
editMenu.addAction("makeRootDocs", self.makeRootDocs)
viewMenu = self.menuBar().addMenu("&View")
selectTabMenu = viewMenu.addMenu("Select &tab")
selectTabMenu.addAction("&Contents", self.focusContents)
selectTabMenu.addAction("&Product", self.focusProduct)
selectTabMenu.addAction("&Object", self.focusObject)
selectTabMenu.addAction("&Document", self.focusDocument)
viewMenu.addAction("&Refresh", self.refresh)
viewMenu.addAction("Add format column", self.selectAndAddFormatColumn)
viewMenu.addAction("Info", self.info)
# viewMenu.addAction("fillFields", self.fillFields)
viewMenu.addAction("showAlbumConnectionTree", self.showAlbumConnectionTree)
toolsMenu = self.menuBar().addMenu("&Tools")
toolsMenu.addAction("&proposeNextAction", self.proposedActionLoop)
toolsMenu.addAction("&Test", self.test)
toolsMenu.addAction("E&ject and Clear", self.ejectAndClear)
toolsMenu.addAction("&Clear", self.clearContents)
toolsMenu.addAction("Add downloaded metadata", h.addMetadata)
toolsMenu.addAction("Fetch data from infosource by EAN", h.fetchDataByEan)
toolsMenu.addAction("Fetch data from infosource by title", h.fetchDataByTitle)
toolsMenu.addAction("readDirInfosources", readDirInfosources)
# File menu ============================================================
def selectMedium(self):
"""Select medium (album object) and display its data."""
f = finddialog.FindDialog("object")
if not f.exec_(): return
objectId = f.selectedId()
if objectId: self.hierarchyWidget.fillFields(objectId=objectId)
def selectMediumViaMediaid(self):
caption = "Find medium via owner's mediaid"
(mediaid, ok) = QInputDialog.getText(self, caption, "Please enter the mediaid",
QLineEdit.Normal, '')
if not ok: return
sql = "SELECT objectid FROM owner WHERE mediaid=%s" % q(mediaid)
objectidCSV = ','.join(db.valueList(sql))
if not objectidCSV:
msgbox("No media with this mediaid found: %s" % mediaid)
return
f = finddialog.FindEntityAndTypeDialog("object")
f.critEdit.setText("!id IN (%s)" % objectidCSV)
f.showCandidates()
if not f.exec_(): return
objectId = f.selectedId()
if objectId: self.hierarchyWidget.fillFields(objectId=objectId)
def selectProduct(self, productId=None):
"""Select product which may be package, and display its data."""
fp = finddialog.FindDialog("product")
if not fp.exec_(): return
productId = fp.selectedId()
if productId: self.hierarchyWidget.fillFields(productId=productId)
def selectSeries(self):
"""Select product, then corresponding medium (object) and display its data."""
seriesId = document.findSeries()
if seriesId: self.hierarchyWidget.fillFields(seriesId=seriesId)
def selectDocument(self):
"""Select document, then corresponding medium (object) and display its data."""
fp = finddialog.FindDialog("document")
if not fp.exec_(): return
documentId = fp.selectedId()
objectId = dbq.getAlbum(documentId)[0]
if objectId:
self.hierarchyWidget.fillFields(objectId=objectId)
return
documentId = db.readSql("SELECT versionof FROM document WHERE id=%s" % documentId, 0, 1)
if not documentId:
msgbox("No album for this document")
return
objectId = dbq.getAlbum(documentId)[0]
if objectId:
self.hierarchyWidget.fillFields(objectId=objectId)
return
msgbox("No album for this document")
#def commitImport(self):
#""" !!! TODO: Also remove album and package.
#Execute scheduled operations, prepared by importDirectory().
#NOT for actions scheduled with Edit - Schedule action. See File -Execute scheduled actions.
#TODO: Take care of *_MP3WRAP.mp3
#id3 tags
#"""
#log = logging.getLogger("commitImport")
#h = self.hierarchyWidget
#dir = h.dirToImport
#caption = "Commit import of directory: %s" % dir
#if h.filesToArchive is None:
#errmsg("No files defined to archive. Call File - Import Directory first.", "Album Form")
#return
#ta = archive.FileDbTransaction("Commit import")
#log.debug("h.filesToArchive: %s" % unicode(h.filesToArchive))
#for origDocId,v in h.filesToArchive.items():
#print(origDocId, v)
#(path, hash, mimetypeId) = v
#log.info("copy data: path: %s, mimetypeId: %s" % (path, mimetypeId))
#dl = db.dicList("SELECT * FROM document WHERE id="+origDocId)
#if not dl:
#errmsg("No entry for doc " + origDocId)
#return
#title = dl[0]["title"]
#try:
#origdoc = document.Document()
#origdoc.readFromDb(origDocId)
#origTypeId = origdoc.vd["mimetypeid"]
#log.info("orig mimetypeId: %s" % origTypeId)
#copydoc = copy.deepcopy(origdoc)
#copydocid = copydoc.getCloneId()
#copydoc.src = path
#copydoc.vd["hash"] = hash
#copydoc.vd["mimetypeid"] = mimetypeId
## m = mimetype.mimetypeFromFile(path) if path else None
## if m: copydoc.vd["mimetypeid"] = m.getId()
## TODO: Create filelist entry at the same time.
#if origTypeId == "122":
#log.info("Creating ISO file...")
## TODO: More general solution to create a file from a directory with several files,
## e.g.: genisoimage, mp3wrap, convert (JPG files to PDF), ...; see askAndWrapAlbum
#ta.crList.append(copydoc.createIso())
#log.info("Created ISO file from %s in %s" % (copydoc.src, copydoc.dest))
#else:
#log.info("copyRename")
#ta.crList.append(copydoc.copyRename())
#ta.sqlList.extend(copydoc.saveExtended()) # also creates cp entry and version rel entry
#except Exception as s: # rollback due to SqlError or file error
#log.error("Exception: %s" % s)
#ta.exceptionHandling(s)
#return
#ta.confirmCommit()
#db.execSql("select nextval('document_id_seq'::regclass);")
## If h.dirToImport is empty, remove it, else warn.
#for root, dirs, files in os.walk(dir, topdown=False):
#if files:
#msgbox("Directory %s not empty, can't remove it." % root, caption)
#return
#for subdirname in dirs:
#os.rmdir(os.path.join(root, subdirname))
#os.rmdir(dir)
def addPackage(self):
"""Create package from user's inputs. Parts will be added later."""
h = self.hierarchyWidget
caption = "Create new package"
msg = textwrap.dedent("""
<html><b>Creating a package of media of the same type</b><br><br>
Here you can enter a new package (e.g. of 2 CDs).<br>
First you enter the title of the package, <br>
then the medium type (e.g. 'CD'),<br>
then the number of media (e.g. 2).<br><br>
The program will then create a product entry from the title<br>
with the appropriate object type (e.g. ot='2 CDs').<br><br>
Subsequently you can enter the parts of the package.<br><br>
Please enter the package title now.</html>
""")[1:-1]
ds = h.lastPackageDataset
(name, ok) = QInputDialog.getText(self, caption, msg, QLineEdit.Normal, ds[0])
if not ok: return
(ean, ok) = QInputDialog.getText(self, caption, "Please enter EAN (or leave blank)", QLineEdit.Normal, ds[1])
if not ok: return
(part_ot, ok) = QInputDialog.getText(self, caption, "Please enter medium type. e.g. CD or DVD",
QLineEdit.Normal, ds[2])
if not ok: return
(num, ok) = QInputDialog.getInteger(self, caption, "Please enter number of %ss in this package" % part_ot,
ds[3], 1, 99, 1)
if not ok: return
msg = textwrap.dedent("""
<p>Please select recording type if appropriate.</p>
<p><dl>
""")[1:-1]
msgBox = QMessageBox()
msgBox.setWindowTitle(caption)
appButton = msgBox.addButton("Application", QMessageBox.ActionRole)
audioButton = msgBox.addButton("Audio", QMessageBox.ActionRole)
videoButton = msgBox.addButton("Video", QMessageBox.ActionRole)
noneButton = msgBox.addButton("None/Other", QMessageBox.ActionRole)
if part_ot == 'CD':
msgBox.setDefaultButton(audioButton)
elif part_ot == 'DVD':
msgBox.setDefaultButton(videoButton)
else:
msgBox.setDefaultButton(noneButton)
msgBox.exec_()
if msgBox.clickedButton() == appButton:
rt = 'Application'
elif msgBox.clickedButton() == audioButton:
rt = 'Audio'
elif msgBox.clickedButton() == videoButton:
rt = 'Video'
else:
rt = ''
h.lastPackageDataset = (name, ean, part_ot, num)
dic = {"name":name, "ot":"%d %ss" % (num, part_ot), "ean":ean, "rt":rt}
id = db.insertReturningId(db.createInsertReturningIdSql("product", dic))
#print "id:", id
# Show package.
instancewidget.showEntityForm(self, "product", id)
def save(self):
"""Save dbTitleColumn content of documents and works to document table.
"""
h = self.hierarchyWidget
for item in traverseRecursive(h.invisibleRootItem()):
level = unicode(item.text(h.levelColumn))
id = getText(item, h.idColumn)
if not id or level not in ("document","work"): continue
sql = db.createUpdateSql("document", {"id":id, "title":getText(item, h.dbTitleColumn)})
db.execSql(sql)
# Save column widths
for i in range(len(h.baseColumnWidths)):
h.baseColumnWidths[i] = h.columnWidth(i)
def makeRootDocs(self):
"""Turn selected docs into root docs.
"""
h = self.hierarchyWidget
for item in traverseRecursive(h.invisibleRootItem()):
if item.isSelected():
level = unicode(item.text(h.levelColumn))
if level not in ("document","work"): continue
id = getText(item, h.idColumn)
print "id:", id
docId = h.getDocId(item)
print "docId:", docId
if not docId:
errmsg("No document ID!")
return
cpids = db.valueList("SELECT id FROM cp WHERE documentid=%s" % docId)
if len(cpids) > 1:
errmsg("There are several copies!")
return
if len(cpids) != 1:
errmsg("Strange, no copy!")
return
sql = "UPDATE cp SET file='/' WHERE id=%s" % cpids[0]
print sql
db.execSql(sql)
self.refresh()
def archivePartDoc(self):
"""
- let user select document (or new file to archive it as in newDocumentFromFile)
- add origdoc and origdoc-cp entries
"""
h = self.hierarchyWidget
objectId = h.objectId # maybe remember others as well?
if not objectId:
msgbox("No object ID")
return
#documentId = document.newDocumentFromFile()
f = finddialog.FindEntityAndTypeDialog("document")
#f.showCandidates()
if not f.exec_(): return
documentId = f.selectedId()
if not documentId: return
copyDoc = db.idDic("document", documentId)
# TODO: function that creates appropriate origDoc from copyDoc
# also usable in importDir
origDoc = document.Document(initDic=copyDoc)
del origDoc.vd["hash"]
del origDoc.vd["size"]
mimetypeId = origDoc.vd["mimetypeid"]
if mimetypeId:
mt = mimetype.Mimetype(mimetypeId)
#if mt.maintype() == "audio": origDoc.vd["mimetypeid"] = '113'
#elif mt.maintype() == "video": origDoc.vd["mimetypeid"] = '112'
origDoc.vd["mimetypeid"] = self.defaultMimetypeId(mt.maintype())
#print origDoc
origDoc.save()
print "new origdoc id:", origDoc.vd["id"]
sql = "UPDATE document SET versionof=%s WHERE id=%s" % (origDoc.vd["id"], documentId)
print sql
db.execSql(sql)
sql = "INSERT INTO cp (objectid, documentid) VALUES (%s, %s)" % (objectId, origDoc.vd["id"])
print sql
db.execSql(sql)
# Edit menu ============================================================
def scheduleAction(self):
h = self.hierarchyWidget
if len(h.formats) == 0:
msgbox("There is no format yet. Please add a format column first.")
return
itemList = []
for item in traverseRecursive(h.invisibleRootItem()):
if item.isSelected():
itemList.append(item)
if not itemList:
msgbox("To schedule an action for an item, please select the item first!")
return
# defines actionChars
#X: export a copy
#V: register existing document as Version
#d: dummy action
listText = textwrap.dedent("""
R: Rip and encode only
N: create New copy: rip, encode and copy to archive
M: create DB record (reMember Metadata)
E: Export document
-: no action, or remove scheduled action
""")[1:-1]
actionList = listText.split('\n')
s = StringSelector(actionList)
if not s.exec_(): return
actionChar = s.selection()[0][0]
if actionChar == '-': actionChar = ' '
# Need formatId even in case of no action, to un-schedule
# a previously scheduled action.
if len(h.formats) == 1:
formatId = h.formats[0].getId()
else:
formatList = []
# Create format string list and try to preselect correct format.
# TODO: Thoroughly test this.
try:
d = h.albumList[0][0][0]
mt = d.vd.get("mimetypeid")
except:
mt = None
if mt == "122": mt = "125" # x-dvd -> x-video-dvd-image
for f in h.formats:
e = "%s: %s" % (f.getId(), f.vd["shortname"])
if f.getId() == mt:
formatList.insert(0, e)
else:
formatList.append(e)
# formatList = ["%s: %s" % (f.getId(), f.vd["shortname"]) for f in h.formats]
s = StringSelector(formatList)
if not s.exec_(): return
res = s.selection()[0]
formatId = getId(res)
#col = h.formatColumn[formatId]
for item in itemList:
h.scheduleSingleAction(item, formatId, actionChar)
def takeTitles(self):
"""Overwrite current document titles with those from external source (in extTitleColumn).
"""
h = self.hierarchyWidget
for item in traverseRecursive(h.invisibleRootItem()):
level = getText(item, h.levelColumn)
if level != "document": continue
extTitle = getText(item, h.extTitleColumn)
item.setText(h.dbTitleColumn, extTitle)
id = getText(item, h.idColumn)
sql = "UPDATE document SET title=%s WHERE id=%s" % (q(extTitle), id)
db.execSql(sql)
def takeTitlesAsFilenames(self):
#For imported files from LP or MC directory, put an 'A' or 'B' before name
h = self.hierarchyWidget
for item in traverseRecursive(h.invisibleRootItem()):
level = getText(item, h.levelColumn)
if level != "document": continue
title = getText(item, h.dbTitleColumn) or getText(item, h.extTitleColumn)
item.setText(h.fileColumn, title)
id = getText(item, h.idColumn)
if not id: continue
sql = "UPDATE cp SET file=%s WHERE documentid=%s" % (q(title), id)
print(sql)
db.execSql(sql)
# self.fillFields()
def createWork(self):
"""For selected documents, create container work and part rels.
Container work is prototypic, an audio work is sound only (SO).
MP3 (MP3WRAP) can additionally exist as its version.
This way the container work, as it is prototypic, it can be official, public,
and has no bias toward a specific format (others may prefer OGG or whatever).
In a proper data structure, the container work is needed to refer to by role records.
Playback of a container work can be done as playback of its archivable version,
or as playback of archivable versions of its parts.
TODO: Calc work title as longest common prefix, part titles as rest
"""
self.save()
h = self.hierarchyWidget
itemList = []; titleList = []
for item in traverseRecursive(h.invisibleRootItem()):
if item.isSelected():
title = getText(item, h.extTitleColumn) or getText(item, h.dbTitleColumn)
if not title or not getText(item, h.idColumn): #List[0]
errmsg("To create a work, every part must have a title and a document record (ID)")
return
itemList.append(item)
titleList.append(title)
if not itemList:
msgbox("To create a container work, please select parts first!")
return
title = commonPrefix(titleList)
# Prevent the 'A' in 'Adagio', 'Allegro', 'Andante' from being misunderstood as part of the title,
# if a work has only parts that begin with 'A'.
if len(title)>2 and title[-1] in 'Aa' and title[-2] in ' /:-,.':
title = title[:-2]
prefixLen = len(title)
if not title:
title = getText(itemList[0], h.extTitleColumn) or getText(itemList[0], h.dbTitleColumn)
firstPartId = getText(itemList[0], h.idColumn)
dic = db.dicList("SELECT summary, contype, date, mimetypeid, licenseid, true as prototypic, languageid "
+ "FROM document WHERE id=" + firstPartId)[0]
while title[-1] in ' /:-,': # Remove trailing punctuation that was between work and part
title = title[:-1]
dic.update({"title":title})
#print dic
sql = db.createInsertReturningIdSql('document', dic)
# "INSERT INTO document " \
# + "SELECT title, summary, contype, date, mimetypeid, licenseid, true as prototypic, languageid " \
# + "FROM document WHERE id=%s RETURNING id" % firstPartId
#print sql
workId = db.insertReturningId(sql)
#print workId
for (i0, item) in enumerate(itemList):
docId = getText(item, h.idColumn)
sql = "INSERT INTO rel (ent1, id1, ord, relid, ent2, id2) VALUES (%d, %s, %d, %d, %d, %s)" \
% (5, docId, i0+1, 44153, 5, workId)
db.execSql(sql)
sql = "UPDATE document SET title=%s WHERE id=%s" % (q(titleList[i0][prefixLen:].strip()), docId)
db.execSql(sql)
self.refresh()
def refresh(self):
h = self.hierarchyWidget
objectId = h.objectId # maybe remember others as well?
self.clearContents()
h.fillFields(objectId=objectId)
def adoptDocuments(self):
"""If there are other objects of the same product,
and those objects have cp entries,
adopt these entries."""
msg = ""
productId = self.productWidget.getId()
if not productId: msg = "No product."
#errmsg("No product")
#return
objectId = self.objectWidget.getId()
#if not objectId:
#errmsg("No object")
#return
if productId and objectId:
sql = "SELECT * FROM object WHERE productid=%s AND id!=%s" % (productId, objectId)
dl = db.dicList(sql)
if len(dl) == 0: msg = "No (other) object of this product"
#errmsg("No other object of this product")
#return
# There are products with the same contents (=same discid) but different EANs.
if msg: # Ask user to choose a product; default criterium: same discid.
if productId:
discid = self.productWidget.getValue("discid")
crit = "discid=%s" % q(discid) if discid else ""
f = finddialog.FindEntityAndTypeDialog("product")
f.helpIntro = "<p>%s Please select a (different) product</p>" % msg
f.critEdit.setText(crit)
f.showCandidates()
if not f.exec_(): return
productId = f.selectedId()
if not productId: return
sql = "SELECT * FROM object WHERE productid=%s" % (productId)
dl = db.dicList(sql)
if len(dl) == 0:
errmsg("No object of this product")
return
descrList = []; detailList = []; sourceId = None
for dic in dl:
sql = "SELECT SUBSTR(file,6,2) || ': ' || title as c FROM v_doc WHERE objectid=%s ORDER BY file;" % dic["id"]
print(sql)
content = ', '.join(db.valueList(sql))
print(objectId, content)
if content:
sourceId = dic["id"]
descrList.append("%s: %s" % (sourceId, dic["descr"]))
detailList.append(content)
if not sourceId:
errmsg("Sorry, no object has cp entries")
return
if len(descrList) > 1:
s = StringDetailSelector(descrList, detailList)
if not s.exec_(): return
sourceId = getId(s.selection()[0])
sql = "INSERT INTO cp (documentid, objectid, file) SELECT documentid, %s, file FROM cp WHERE objectid=%s ORDER BY file;" % (objectId, sourceId)
# print sql
db.execSql(sql)
self.refresh()
def openSeries(self):
"""Select or create a series document.
The following albums will be expected to be episodes of this series.
No more than one series can be "open" at a time.
When AlbumForm is closed, the series is automatically closed (forgotten).
To close a series explicitly in an AlbumForm session, closeSeries must be called.
TODO:
If a new album is to be registered and a series is open,
ask the user if this is the next episode of the open series.
Find the number n of the last registered episode (from rel.ord?)
and propose n+1 as the default episode number.
If the user agrees that this is an episode, remember it.
In scheduleDefaultActions, if this is an episode, propose to
register the whole album as one document.
Create a rel between album origdoc and series doc.
See node 3947: Document series
"""
caption = "Open series"
h = self.hierarchyWidget
#item = self.getAlbumItem()
#objectId = getText(item, h.idColumn)
#rootSql = "SELECT d.id FROM document d, cp WHERE d.id=cp.documentid AND objectid=" + objectId
#rootDocId = db.readSql(rootSql) # Root document ID is read from DB.
#seriesSql = "SELECT id2 FROM rel WHERE relid=63832 AND ent1=5 AND ent2=5 GROUP BY id2 ORDER BY id2 DESC LIMIT 20;"
#crit = "!id IN (%s)" % ','.join(db.valueList(seriesSql))
f = finddialog.FindEntityAndTypeDialog('document')
# f.helpIntro = "<p>Please select or create a package</p>"
# f.critEdit.setText(crit)
# f.order = "id DESC"
# f.newRecordValue = packageTitle
# f.initDic = package
# f.showCandidates()
if not f.exec_(): return
h.series = f.selectedId()
def closeSeries(self):
h = self.hierarchyWidget
h.series = None
def makeEpisode(self):
"""Make document (root doc of album or package) episode of a series.
See node 3947: Document series for a definition of series.
"""
caption = "Turn document into episode of a series"
h = self.hierarchyWidget
#item = self.getAlbumItem()
item = h.currentItem()
level = getText(item, h.levelColumn)
if level not in ("package", "album"):
msg = textwrap.dedent("""
This is only possible for album or package root documents.
In the rare case of a document from a package is an episode,
usually the package as a whole is the episode,
not a part album of it.
So in case of a single album, select that,
and if there is a package, usually select the package,
and try again.""")
msgbox(msg, caption)
return
rootDocId = h.getRootDocId(item)
# rootSql = "SELECT d.id FROM document d, cp WHERE d.id=cp.documentid AND objectid=" + objectId
# rootDocId = db.readSql(rootSql) # Root document ID is read from DB.
if not rootDocId:
# TODO: idea is good, implementation BROKEN!
# cp gets productid instead of objectid
if level == "package":
packageId = getText(item, h.idColumn)
descr = getText(item, h.dbTitleColumn)
dic = {"descr":descr, "productid":packageId}
objectId = db.createOrFindRecord("object", dic, "productid=%s" % packageId)
if not objectId: return
else:
objectId = getText(item, h.idColumn)
msg = "There is no root document yet. Create?"
if QMessageBox.question(None, caption, msg,
QMessageBox.StandardButtons(QMessageBox.No | QMessageBox.Yes),
QMessageBox.Yes) == QMessageBox.No: return
rootDocId = h.createNewRecord("01", "01", item, item)
if not rootDocId: return
seriesSql = "SELECT id2 FROM rel WHERE relid=63832 AND ent1=5 AND ent2=5 GROUP BY id2 ORDER BY id2 DESC LIMIT 20;"
crit = "!id IN (%s)" % ','.join(db.valueList(seriesSql))
f = finddialog.FindEntityAndTypeDialog('document', msg="Please select series")
# f.helpIntro = "<p>Please select or create a package</p>"
f.critEdit.setText(crit)
f.order = "id DESC"
# f.newRecordValue = packageTitle
# f.initDic = package
f.showCandidates()
if not f.exec_(): return
seriesId = f.selectedId()
maxOrdSql = "SELECT max(ord) FROM rel WHERE relid=63832 AND ent1=5 AND ent2=5 AND id2=" + seriesId
maxOrd = db.readSql(maxOrdSql)
default = int(maxOrd)+1 if maxOrd else 0
(ord, ok) = QInputDialog.getInteger(None, caption, "Please enter order in series", default, 0, 9999, 1)
if not ok: return
relDic = {"ent1":"5", "id1":rootDocId, "ord":ord, "relid":"63832", "ent2":"5", "id2":seriesId}
db.insertDic("rel", relDic)
def removeEpisode(self):
"""Remove episode document from series.
"""
caption = "Remove document from series"
h = self.hierarchyWidget
item = h.currentItem()
level = getText(item, h.levelColumn)
if level not in ("package", "album"):
msg = textwrap.dedent("""
This is only possible for album or package root documents.
In the rare case of a document from a package is an episode,
usually the package as a whole is the episode,
not a part album of it.
So in case of a single album, select that,
and if there is a package, usually select the package,
and try again.""")
msgbox(msg, caption)
return
rootDocId = h.getRootDocId(item)
if not rootDocId:
msgbox("No root doc")
return
removeSql = "DELETE FROM rel WHERE relid=63832 AND ent1=5 AND ent2=5 AND id1=%s" % rootDocId
db.execSql(removeSql)
id = getText(item, h.idColumn)
if level == "album":
h.fillFields(objectId=id)
else:
h.fillFields(productId=id)
def getFreeDbTitles(self):
"""Get album and track titles from FreeDB.
User needs to manually provide discid.
TODO: generalize
"""
caption = "getFreeDbTitles"
h = self.hierarchyWidget
discid = h.discid
if not discid: discid = self.productWidget.getValue("discid")
msg = "Please enter or confirm the discid:"
(discid, ok) = QInputDialog.getText(None, caption, msg, QLineEdit.Normal, discid)
if not ok: return
(genre,ok) = QInputDialog.getText(self, caption, "Enter Genre (misc, rock, data etc.)",
QLineEdit.Normal, "misc")
if not ok: return
#discid = unicode(qdiscid)
fbase = "http://www.freedb.org/freedb/%s/%s" % (unicode(genre), unicode(discid))
opener1 = urllib2.build_opener()
opener1.addheaders = [('User-agent', 'Mozilla/5.0')]
try:
f = opener1.open(fbase)
except urllib.error.HTTPError:
errmsg("HTTPError")
return
s = unicode(f.read(),"latin1") #"utf-8")
f.close()
ttitle = {}
for line in s.splitlines():
if line.startswith("DTITLE="):
dtitle = line[7:]
elif line.startswith("TTITLE"):
parts = line.split('=')
i = int(parts[0][6:])
# print i
ttitle[i] = parts[1]
#print dtitle
#print ttitle
for item in traverseRecursive(h.invisibleRootItem()):
level = getText(item, h.levelColumn)
rn = getText(item, h.rnColumn)
if level == "album": # and getText(item, h.fileColumn) == '/':
item.setText(h.extTitleColumn, dtitle)
elif level == "document":
item.setText(h.extTitleColumn, ttitle[int(rn)-1])
def getTitlesFromFile(self):
"""Get track titles from file.
"""
caption = "getTitlesFromFile"
h = self.hierarchyWidget
path = archive.selectToarchiveFile()
#path = "/tmp/tracks"
f = open(path, 'r')
s = unicode(f.read(),"utf-8")
f.close()
lines = []
#i = 1
for line in s.splitlines():
lines.append(line)
#print i, line
#i += 1
for item in traverseRecursive(h.invisibleRootItem()):
level = getText(item, h.levelColumn)
rn = getText(item, h.rnColumn)
try:
c = int(rn)
except:
continue
if level == "document":
if c > len(lines):
print "No input line for track %d" % c
else:
item.setText(h.extTitleColumn, lines[c - 1])
def setObjectOwner(self, objectId):
# log.info("Finding or creating owner of object %s" % objectId)
h = self.hierarchyWidget
ownerEntries = db.dicList("SELECT * FROM owner WHERE objectid=%s" % objectId)
if ownerEntries:
msgbox("This object already has one or more owner entries")
return
sql = "SELECT * FROM owner WHERE posdate IS NOT NULL ORDER BY posdate DESC, id DESC LIMIT 1;"
dl = db.dicList(sql)
if not dl:
msgbox("No default")
return
d = dl[0]
dic = {"posdate":isoNow()[:10], "objectid":objectId}
if d["personid"]: dic.update({"personid":d["personid"]})
if d["companyid"]: dic.update({"companyid":d["companyid"]})
if h.mediaid: dic.update({"mediaid":str(h.mediaid)})
print(dic)
db.insertDic("owner", dic)
# TODO: Refresh and focus
def setPackageOwner(self):
caption = "Set owner of package"
log = logging.getLogger("setPackageOwner")
h = self.hierarchyWidget
item = h.currentItem()
# sql = "SELECT packageid FROM product p, object o WHERE p.id=o.productid AND o.id=%s"
packageId = getText(item, h.idColumn)
log.info("Creating or finding object for package %s" % packageId)
descr = getText(item, h.dbTitleColumn)
dic = {"descr":descr, "productid":packageId, "orig":"true"}
objectId = db.createOrFindRecord("object", dic, "productid=%s" % packageId)
if not objectId: return None
self.setObjectOwner(objectId)
return objectId
def setOwner(self):
"""Set owner of current album. Take owner of last album as default
and switch to object widget."""
caption = "Set owner of album"
log = logging.getLogger("setOwner")
h = self.hierarchyWidget
#item = self.getAlbumItem()
item = h.currentItem()
if getText(item, h.levelColumn) not in ("album", "package"):
msgbox("Please select an album or package for this.")
return
if getText(item, h.levelColumn) == "package":
objectId = self.setPackageOwner()
else:
objectId = getText(item, h.idColumn)
self.setObjectOwner(objectId)
if objectId:
h.fillFields(objectId=objectId)
self.focusObject()
def getRootDocItemList(self):
"""Return list of items of album level where file='/'."""
h = self.hierarchyWidget; res = []
for item in traverseRecursive(h.invisibleRootItem()):
level = getText(item, h.levelColumn)
if level == "album" and getText(item, h.fileColumn) == '/':
res.append(item)
return res
def getToplevelItemList(self):
h = self.hierarchyWidget; res = []
for i in range (h.topLevelItemCount()):
res.append(h.topLevelItem(i))
return res
def getCurrentItemList(self):
h = self.hierarchyWidget; res = []
for item in traverseRecursive(h.invisibleRootItem()):
cur = getText(item, h.curColumn)
if cur == ' ': continue
res.append(item)
return res
def getScheduledActionList(self):
"""Return list of tuples (item, format, actionChar).
Used to find out if and which actions are scheduled, and by execScheduledActions().
Loop over items, and over formats per item.
"""
# caption = "Executing scheduled actions"
# log = logging.getLogger("execScheduledActions")
h = self.hierarchyWidget
# archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
# First collect scheduled items with their formats into a list for progress bar.
result = []
for item in traverseRecursive(h.invisibleRootItem()):
for destFormat in h.formats:
formatId = destFormat.getId()
formatColumn = h.formatColumn[formatId]
actionChar = (getText(item, formatColumn) + " ")[2]
if actionChar != ' ':
result.append((item, destFormat, actionChar))
return result
def execScheduledActions(self):
"""Execute scheduled actions (creating copies etc.).
Loop over items, and over formats per item.
The hierarchyWidget holds with the document hierarchy levels
(series, package, album, side, work, document) also the scheduled actions
for these levels. One action per level and format can be scheduled.
One could imagine to also allow for several different actions
per level and format, such as N and E (create a new copy and export it).
Of the many theoretically conceivable options, we had to first
implement those which are most needed. When deciding what to implement,
it is also important to pay attention to maintain a clear design,
otherwise the program easily gets unmaintainable.
The actions could be executed in several different orders.
One can loop over:
- the items in the hierarchy (i.e., vertically in the table)
- the formats (i.e., horizonally in the table)
- the actionCharSet elements
(for example first all copy actions, then all export actions, etc.)
Currently the following order is implemented:
for item in traverseRecursive(h.invisibleRootItem()):
for format in h.formats:
The action traversal is linearized by:
actionList = self.getScheduledActionList()
and then the actionList is traversed.
"""
caption = "Executing scheduled actions"
log = logging.getLogger("execScheduledActions")
h = self.hierarchyWidget
actionList = self.getScheduledActionList()
steps = len(actionList)
log.info("steps: %d" % steps)
actionCharSet = set([e[2] for e in actionList])
if 'E' in actionCharSet: # for export, select target directory
destDir = archive.selectDir(
"Select target directory for export", "export")
if not destDir: return
multiProgress = progress.MultiProgressDialog(levels=2, windowTitle="Unibas: " + caption,
labelText="<html>%s</html>" % caption)
multiProgress.show()
w0 = multiProgress.progressWidgetList[0]
w0.label.setText("%d actions" % len(actionList))
itemCount = canceled = success = 0
for (item, destFormat, actionChar) in actionList:
w0.progressBar.setValue(100 * itemCount / len(actionList))
# These variables are needed by most actions.
albumItem = h.getAncestorAlbumItem(item)
rn = getNumOr1(item, h.rnColumn)
dn = getNumOr1(item, h.dnColumn)
log.info("Exec %s action on item %s (count: %d) with format %s"
% (actionChar, rn, itemCount, destFormat))
level = getText(item, h.levelColumn)
origDocId = h.getDocId(item)
title = unicode(getText(item, h.dbTitleColumn)
or getText(item, h.extTitleColumn))
filename = "track%02d" % int(dn) \
if h.maintype == "audio" and level == "document" \
else archive.createFilename(title)
destPath = archive.getFreeToarchiveFilepath(
u"{}.{}".format(filename, destFormat.ext()))
albumObjectId = getText(albumItem, h.idColumn)
albumTitle = getText(albumItem, h.dbTitleColumn)
albumDescr = db.descrWithId("object", albumObjectId)
# albumCnt is counter of albums in package, 1-based;
albumCnt = getNumOr1(albumItem, h.rnColumn)
# log.debug("albumObjectId=%s, albumCnt=%d, albumTitle=%s" % (albumObjectId, albumCnt, albumTitle))
log.debug(ds(locals(), "albumObjectId,albumCnt,albumTitle"))
(origDoc, copyDoc) = h.docsToA.getAlbumDocPair(albumCnt, level, rn)
if actionChar == 'R': # ripEncode can be used for testing, leaves no DB traces.
success = h.ripEncode(rn, dn, item, level, origDocId, title,
filename, destPath, destFormat, albumItem,
albumObjectId, albumTitle, albumDescr, albumCnt,
multiProgress)
elif actionChar == 'N':
# success = h.createNewCopy(rn, item, destFormat, albumItem, multiProgress)
success = h.createNewCopy(rn, dn, item, level, origDocId,
title, filename, destPath, destFormat,
albumItem, albumObjectId, albumTitle, albumDescr, albumCnt,
multiProgress)
elif actionChar == 'M':
success = h.createNewRecord(rn, dn, item, albumItem) # TODO: check objectId; better albumItem?
elif actionChar == 'E': # also useful for testing
success = h.exportCopy(rn, dn, item, level, origDocId, title, filename,
destPath, destFormat, albumItem, destDir, multiProgress)
elif actionChar == 'd':
self.dummyAction(rn, item)
else:
msgbox("Action not yet implemented: " + actionChar)
# if progress.wasCanceled():
# break
itemCount += 1
#progress.done(0)
if success:
log.info("Finished.")
if h.dirToImport:
msg = "Delete original directory with contents?\n" + h.dirToImport
if QMessageBox.question(None, caption, msg,
QMessageBox.StandardButtons(QMessageBox.No | QMessageBox.Yes),
QMessageBox.Yes) == QMessageBox.No: return
shutil.rmtree(h.dirToImport)
#if h.discid: # TODO: Reactivate later
#btc("eject " + h.device)
return
log.info("Canceled.")
# View menu ============================================================
def focusContents(self):
print("self.tabWidget.setCurrentWidget(self.contentsPage)")
self.tabWidget.setCurrentWidget(self.contentsPage)
def focusProduct(self):
self.tabWidget.setCurrentWidget(self.productWidget)
def focusObject(self):
self.tabWidget.setCurrentWidget(self.objectWidget)
def focusDocument(self):
self.tabWidget.setCurrentWidget(self.docWidget)
def selectAndAddFormatColumn(self):
h = self.hierarchyWidget
crit = "support=1 AND id not in (%s)" % ','.join(['0'] + [f.getId() for f in h.formats])
if h.maintype: crit += " AND type~'%s/'" % h.maintype
fm = finddialog.FindEntityAndTypeDialog('mimetype')
fm.critEdit.setText(crit)
fm.showCandidates()
if not fm.exec_(): return
id = fm.selectedId()
self.addFormatColumn(id)
def info(self):
msgbox("discid from DB: " + self.productWidget.getValue("discid"))
#def refresh(self):
#objectId = self.objectId
#self.clearContents()
#self.hierarchyWidget.fillFields(objectId=objectId)
# Tools menu ============================================================
def test(self):
caption = "TEST"; h = self.hierarchyWidget
# Working PercentageProcess reading example:
multiProgress = progress.MultiProgressDialog()
multiProgress.show()
btc("rm /tmp/wrap_MP3WRAP.mp3")
command = "mp3wrap /tmp/wrap.mp3 /unibas_archive/oid10645/Fi*.mp3"
p = PercentageProcess(multiProgress, "RipEncode", command,
".* +(\d+) ?%.*", shell=True)
p.startCmd()
print "after startCmd()"
return
# Working DestinationFileProcess example:
multiProgress = progress.MultiProgressDialog()
multiProgress.show()
destPath = '/tmp/cd'; endValue = 700000000
cmd = "dd if=/dev/sr1 of=" + destPath
p = DestinationFileProcess(multiProgress, "RipEncode " + destPath,
cmd, destPath, endValue, shell=True)
p.startCmd()
print "after startCmd()"
return
# Percentage reading example, HandBrakeCLI via ripEncode:
multiProgress = progress.MultiProgressDialog()
multiProgress.show()
h.maintype, h.discid, h.device = "video", '1', "/dev/sr1"
h.objectInDevice = h.objectId = '0'
destFormat = mimetype.Mimetype('133'); destFormat.readFromDb()
h.ripEncode("01", "01", 0, "document", 0, "Dummy", "test.mp4",
"/tmp/test.mp4", destFormat, 0, 0, 0, 0, 0, multiProgress)
print "after ripEncode()"
return
# Reading cdparanoia output fails.
#cmd = "cdparanoia -Z -d /dev/sr1 01 /tmp/track.wav" #- | lame - '/unibas_archive/toarchive/track01.mp3'"
#p = DestinationFileProcess(multiProgress, "RipEncode")
#rs = ".*(\[.*>.*\|).*"
#itemCount = canceled = success = 0
#for (item, format, actionChar) in actionList:
#w0.progressBar.setValue(100 * itemCount / len(actionList))
#albumItem = h.getAncestorAlbumItem(item)
##progress.setValue(itemCount)
##canceled = progress.wasCanceled()
##if canceled: break
#rn = getText(item, h.rnColumn)
#log.info("Exec %s action on item %s (count: %d) with format %s"
#% (actionChar, rn, itemCount, format))
#if actionChar == 'R':
#success = h.ripEncode(rn, item, format, albumItem, multiProgress)
#elif actionChar == 'N':
#success = h.createNewCopy(rn, item, format, albumItem, multiProgress)
#elif actionChar == 'M':
#success = h.createNewRecord(rn, item, albumItem) # TODO: check objectId; better albumItem?
#elif actionChar == 'E':
#success = h.exportCopy(rn, item, format, albumItem, destDir, multiProgress)
#elif actionChar == 'd':
#self.dummyAction(rn, item)
#else:
#msgbox("Action not yet implemented: " + actionChar)
## if progress.wasCanceled():
## break
#itemCount += 1
def ejectAndClear(self):
"""Eject drive and clear."""
h = self.hierarchyWidget
if not h.setDevice(): return
bt("eject " + h.device)
h.clear()
self.productWidget.setId(None)
self.objectWidget.setId(None)
def clearContents(self):
"""Remove all items."""
self.hierarchyWidget.clear()
self.productWidget.setId(None)
self.objectWidget.setId(None)
def dummyAction(self, rn, item):
"""Create new copy of document (e.g. audio track) referenced by item (number: rn), in given format.
"""
caption = "dummyAction"
h = self.hierarchyWidget
btc("sleep 1")
# Commands that produce audio to stdout:
# "mplayer -vo null -vc dummy -af resample=44100 -ao pcm -ao pcm:waveheader:file=/dev/fd/3 3>&1 1>&2 %s" % wmafile
# mplayer src.wma -ao pcm -ao pcm:file=file.wav
# "cdparanoia -d %s %s -" % (device, rn)
# Operations:
# - rip or convert to uncompressed standard format
# - pack or unpack
# - encode in dest. format
# - delete sources
def copyAlbumToArchive(self):
"""Copy album (user selects if there are several) files to archive.
"""
h = self.hierarchyWidget
albumItem = self.getAlbumItem()
origdocidList = []
for item in traverseRecursive(albumItem):
if unicode(item.text(h.levelColumn)) != "document": continue
title = unicode(item.text(h.dbTitleColumn))
origdocid = unicode(item.text(h.idColumn))
print(origdocid, title) #, document.getDigitalVersion(origdocid)
origdocidList.append(origdocid)
document.copyArchivableVersionsToActiveMedium(origdocidList)
def showOwnMediaCounts(self):
"""An albumColumn labeled CD or DVD, MC, LP etc. exists if there are media (original or not)
of one's own, i.e. with roomid NOT NULL. For each such album
(maybe several are displayed in case of packages),
there is an entry in this column that tells the number of own media."""
h = self.hierarchyWidget
for item in traverseRecursive(h.invisibleRootItem()):
if unicode(item.text(h.levelColumn)) != "album": continue
id = unicode(item.text(h.idColumn))
if not id: continue
print(unicode(item.text(h.dbTitleColumn)), ("SELECTED" if item.isSelected() else ''))
def emptyItem(self,item):
"""Remove the children of item."""
for index in range(item.childCount()):
# self.emptyItem(item.child(index))
# item.takeChild(index)
self.emptyItem(item.child(0))
item.takeChild(0)
def setId(self,id):
"""Set ID of object (album).
Clear contents and reread it, starting from product id.
"""
h = self.hierarchyWidget
if id and id == h.objectId: return
# if self.isSubmaster and self.id:
#self.clearContents()
h.clear()
h.objectId = id
print("h.objectId = ", h.objectId)
self.fillFields()
self.emit(SIGNAL("changedId"),id)
def proposedActionLoop(self):
while self.proposeNextAction():
pass
def proposeNextAction(self):
"""Propose next actions and let user decide what to do after fillFields().
Actions can be: impossible, possible but not recommended, possible and recommended:
Import dir if:
- There are dirs to import (type will be decided later)
- cleared, i.e. no h.discid or h.filesToArchive
Download metadata and store in infosourcecontent if:
- no metadata yet in infosourcecontent
- EAN given
- type DVD (h.ot == "DVD"; others later)
Add metadata from infosourcecontent to product and document if:
- EAN given
- metadata for EAN in infosourcecontent
- not yet added, e.g. no cover image for product
Prepare album if:
- source is directory, not (yet) disk: True if h.filesToArchive
- currently only single DVDs: if len(itemList) != 1: return
- metadata have been added
Make copies and remove from schedule if:
- copies are scheduled: self.getScheduledActionList()
- DVD from single dir, and album is prepared, or
- from disc (h.discid)
Eject and clear if:
- disc in drive
- no copies scheduled: self.getScheduledActionList()
Delete dir and clear if:
- no copies scheduled
- for all docs (keys) in h.filesToArchive, there is an origdoc and a copydoc
"""
h = self.hierarchyWidget
archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
if h.discid:
originText = "You have checked a disc which can be imported."
elif h.dirToImport:
originText = u"The directory: {}".format(self.dirToImport)
originText += u"has been analyzed and can be imported."
else:
originText = "NIX" # TODO
msg = textwrap.dedent(u"""
<p>{}<br>Please choose one of the proposed actions to do next:</p>
<p><dl>
""")[1:-1].format(originText)
msgBox = QMessageBox()
msgBox.setWindowTitle("Album Form: What to do next?")
itemList = self.getRootDocItemList()
clearButton = None
if itemList:
msg += textwrap.dedent("""
<dt><b>Clear</b></dt>
<dd>Clear display and make ready for next album.</dd>
""")[1:-1]
clearButton = msgBox.addButton("Clear", QMessageBox.ActionRole)
msgBox.setDefaultButton(clearButton)
# Check if importDir is possible and recommended
importDirButton = None
if not h.topLevelItemCount(): # importDir is possible
# Add text to message here.
msg += textwrap.dedent("""
<dt><b>Import directory</b></dt>
<dd>Import album stored in a HD directory.</dd>
""")[1:-1]
importDirButton = msgBox.addButton("Import directory", QMessageBox.ActionRole)
l = importParentDirList
#h.lastImportedType = "DVD"
if h.lastImportedType and h.lastImportedType in l:
l.remove(h.lastImportedType)
l.insert(0, h.lastImportedType)
# print l
for parentDir in l: # TODO: complete list, in settings?
if getSubdirList(archivePath + "raw/" + parentDir):
msgBox.setDefaultButton(importDirButton)
#recommended = True
break
v = self.productWidget.getValue
(productId, ean) = (v("id"), v("ean"))
docId = h.getRootDocId(itemList[0]) if len(itemList) == 1 else None
icList = None; metadataAdded = False
addMetadataButton = downloadMetadataButton = None
if ean:
sql = "SELECT * FROM infosourcecontent WHERE publicid=%s" % q(ean)
print(sql)
icList = db.dicList(sql) # infosourcecontentlist; if icList then metadata are available
print(icList)
if icList: # There are metadata, now check if they are already added.
review = self.docWidget.documentWidget.getValue("review")
if review.strip(): metadataAdded = True
if not metadataAdded: # Recommend adding metadata
msg += textwrap.dedent("""
<dt><b>Add metadata</b></dt>
<dd>Metadata are already downloaded to intermediate tables,
so they can and should be added now.</dd>
""")[1:-1]
addMetadataButton = msgBox.addButton("Add metadata", QMessageBox.ActionRole)
msgBox.setDefaultButton(addMetadataButton)
else: # EAN given and no metadata yet in infosourcecontent
if h.ot == "DVD": # Recommend to Download metadata and store in infosourcecontent if:
msg += textwrap.dedent("""
<dt><b>Download metadata</b></dt>
<dd>Metadata can be downloaded to intermediate tables,
so they can be added later.</dd>
""")[1:-1]
downloadMetadataButton = msgBox.addButton("Download metadata", QMessageBox.ActionRole)
msgBox.setDefaultButton(downloadMetadataButton)
# Prepare album if:
# - source is directory, not (yet) disk: True if h.filesToArchive
# - currently only single DVDs: if len(itemList) != 1: return
# - metadata have been added
prepareAlbumButton = None
# path = h.filesToArchive[docId][0] if docId and h.filesToArchive else ''
path = h.dirToImport or ''
#print("path:", path)
cm = containsMetadata(path)
print("cm:", unicode(cm))
if os.path.exists(path) and len(itemList) == 1:
msg += textwrap.dedent("""
<dt><b>Prepare album</b></dt>
<dd>Metadata can be written to the copy of the album itself,
so it is available without requiring the database.</dd>
""")[1:-1]
prepareAlbumButton = msgBox.addButton("Prepare album", QMessageBox.ActionRole)
if metadataAdded and not cm: msgBox.setDefaultButton(prepareAlbumButton)
# Make copies and remove from schedule if:
# - copies are scheduled: self.getScheduledActionList()
# - DVD from single dir, and album is prepared, or
# - from disc (h.discid)
execActionsButton = None
if self.getScheduledActionList():
msg += textwrap.dedent("""
<dt><b>Execute actions</b></dt>
<dd>Default actions scheduled for new albums are to copy their contents
to files in the default format.</dd>
""")[1:-1]
execActionsButton = msgBox.addButton("Execute actions", QMessageBox.ActionRole)
if cm: msgBox.setDefaultButton(execActionsButton)
##sure if: prod.image, doc.actor
##sure not if: no doc, no ean, no doc.length, no review
# itemList = self.getRootDocItemList()
## if len(itemList) != 1: return
## item = itemList[0]
## docId = self.getRootDocId(item)
#
## docId = self.getRootDocId(itemList[0]) if len(itemList) == 1 else None
#
## length = self.docWidget.documentWidget.getValue("length")
# review = self.docWidget.documentWidget.getValue("review")
# metadataAdded = True if review.strip() else False
#
## DVD album dir is prepared if it contains files, not only *_TS subdirs: getFileList(dir)
## to check if for all docs (keys) in h.filesToArchive, there is an origdoc and a copydoc
# document.getArchivableVersions(origDocId)
msgBox.setText(msg + "</dl></p>")
# keepOrigButton = msgBox.addButton("Keep Original", QMessageBox.ActionRole)
# keepModifButton = msgBox.addButton("Keep Modified File", QMessageBox.ActionRole)
# keepBothButton = msgBox.addButton("Keep Both", QMessageBox.ActionRole)
abortButton = msgBox.addButton(QMessageBox.Abort)
msgBox.exec_()
if msgBox.clickedButton() == abortButton: return 0
elif msgBox.clickedButton() == importDirButton: self.importDirectory()
elif msgBox.clickedButton() == addMetadataButton: self.addMetadata()
elif msgBox.clickedButton() == downloadMetadataButton: self.fetchDataByEan()
elif msgBox.clickedButton() == prepareAlbumButton: self.prepareAlbum()
elif msgBox.clickedButton() == execActionsButton: self.execScheduledActions()
elif msgBox.clickedButton() == clearButton: self.clearContents()
return 1
def showAlbumConnectionTree(self):
h = self.hierarchyWidget
item = h.currentItem() or self.getAlbumItem()
if not item: return
level = getText(item, h.levelColumn)
id = getText(item, h.idColumn)
if id:
entityDic = {"package":"product", "album":"product", "work":"document", "document":"document"}
if not level in entityDic:
errmsg("Unknown entity for level " + level)
return
connectiontree.ConnectionTreeForm(self, entityDic[level], id, 'id').show()
return
id = self.productWidget.getId()
if id and id!='0':
connectiontree.ConnectionTreeForm(self, "product", id, 'id').show()
return
id = self.objectWidget.getId()
if id and id!='0':
connectiontree.ConnectionTreeForm(self, "object", id, 'id').show()
def getFormatIds(self, dir, filelist):
"""Get format IDs from a list of files."""
ids = set()
for path in filelist:
format = mimetype.mimetypeFromFile(dir+'/'+path)
ids.add(format.getId())
return ids
def readDirInfosources():
"""Read directories to be archived under raw/DVD/, raw/CD/ etc.
and fetch metadata for them from infosources,
for those where EANs are provided.
TODO: Prepare this for the case where getPageByEan returns list page, see infosource.getPageByEan
"""
caption = "readDirInfosources"
log = logging.getLogger("readDirInfosources")
archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
rawdir = archivePath + "raw/"
il = infosource.infosourceList("type='bookseller'",["re_d_title"])
for otdir in ["DVD"]: #, "CD", "MC", "LP", "book"]:
otpath = rawdir + otdir
if not os.path.isdir(otpath): continue
albumdirList = getSubdirList(otpath) or [] # album dir paths
for dir in albumdirList:
dirOnly = dir.split('/')[-1] # Selected directory name only.
productDic = readProductDir(dirOnly)
if not "ean" in productDic or not productDic["ean"]: continue
ean = productDic["ean"]
#print ean, dirOnly
sql = "select id from infosourcecontent where publicid=%s" % q(ean)
#print sql
#if db.valueList(sql): continue # For this EAN, metadata are already available.
#print "For EAN %s there are no data yet, fetching them." % ean
for i in il:
#print "-------- infosource:", i
msg = "Fetching data for %s from %s %s" % (dirOnly, i.type, i.name)
#print "ID:", i.id
#print msg
log.info(msg)
p = i.getPageByEan(ean)
idList = i.recordDetail(p,'product','ean')
log.info("recordDetail result: " + unicode(idList))
print("recordDetail result: " + unicode(idList))
def readProductDir(dir, src=None):
"""Return dic containing data from directory name.
The directory name can be like: Giving:
ean1234567890123 Godzilla ean="1234567890123" title="Godzilla"
oid12345 Godzilla objectid="12345" title="Godzilla"
pid12345 Godzilla id="12345" (of product) title="Godzilla"
isxn1234567890 Godzilla isxn="1234567890" title="Godzilla"
isbn1234567890 Godzilla isxn="1234567890" title="Godzilla"
Godzilla title="Godzilla"
ICE_AGE_4_CONTINENTAL_DRIFT volumelabel="ICE_AGE_4_CONTINENTAL_DRIFT" title="Ice Age 4 Continental Drift"
"""
dir = dir.split('/')[-1]
m = re.compile("^([0-9X]{13}) (.*)").match(dir)
if m: return {"ean":m.group(1), "title":m.group(2)}
m = re.compile("^ean([0-9X]+) (.*)").match(dir)
if m: return {"ean":m.group(1), "title":m.group(2)}
m = re.compile("^oid([0-9]+) (.*)").match(dir)
if m: return {"objectid":m.group(1), "title":m.group(2)}
m = re.compile("^pid([0-9]+) (.*)").match(dir)
if m: return {"id":m.group(1), "title":m.group(2)}
m = re.compile("^is[xbs]n([0-9]+) (.*)").match(dir)
if m: return {"isxn":m.group(1), "title":m.group(2)}
m = re.compile("^([A-Z0-9_]+)$").match(dir)
if m: return {"volumelabel":m.group(1),
"title":m.group(1).title().replace('_', ' ')}
return {"title":dir}
def readDocFilename(path, defaultTitle="(Unknown)"):
"""Return dic containing data from file name.
The file name can be like: Giving:
track12.cdda file="track12.cdda"
A01 Tomorrow side="A" title="Tomorrow"
A 12:34:56 Tomorrow file="A 12:34:56" title="Tomorrow"
Godzilla title="Godzilla"
01 First chapter title="First chapter" (01 is ignored, used for order only)
"""
filename = path.split('/')[-1] #.replace('_', ' ')
m = re.compile("^(track\d\d.cdda)").match(filename)
if m: return {"file":m.group(1)}
m = re.compile("^([AB]?)\d\d? ?(.*)").match(filename)
if m: return {"side":m.group(1), "title":archive.fileToTitle(m.group(2), defaultTitle)}
m = re.compile("^([AB] \d\d:\d\d:\d\d) ?(.*)").match(filename)
if m: return {"file":m.group(1), "title":archive.fileToTitle(m.group(2), defaultTitle)}
return {"title":archive.fileToTitle(filename)}
def isDvdStructure(dir, parentDir=None):
"""Return True if dir is a DVD directory structure, containing VIDEO_TS.
If parentDir is given, also check that is "DVD".
"""
if parentDir and parentDir != "DVD": return False
subdirList = getSubdirList(dir) or []
if ("VIDEO_TS" in subdirList or "video_ts" in subdirList):
return True
return False
def selectDir(lastImportedType):
caption = "Select directory to import"
# startdir = settings.getSetting("AlbumForm/importBaseDir")
# if not startdir:
archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
startdir = archivePath + "raw/"
if lastImportedType and lastImportedType in importParentDirList:
startdir += lastImportedType + '/'
subdirList = getSubdirList(startdir)
if subdirList: startdir += subdirList[0] + '/'
dialog = QFileDialog(None, caption, startdir.encode("utf-8"))
dialog.setFileMode(QFileDialog.Directory)
if not dialog.exec_(): return None
dir = unicode(dialog.selectedFiles()[0])
# settings.setSetting("AlbumForm/importBaseDir", os.path.dirname(dir))
return dir
def selectFile(lastImportedType):
caption = "Select file to import"
archivePath, activePath, activeMedium, mediumPattern = archive.getArchiveSettings()
startdir = archivePath + "raw/"
if lastImportedType and lastImportedType in importParentDirList:
startdir += lastImportedType + '/'
#subdirList = getSubdirList(startdir)
#if subdirList: startdir += subdirList[0] + '/'
dialog = QFileDialog(None, caption, startdir.encode("utf-8"))
dialog.setFileMode(QFileDialog.ExistingFile)
if not dialog.exec_(): return None
selectedFile = unicode(dialog.selectedFiles()[0])
return selectedFile
def getProductidFromObjectidInDic(product):
if not "objectid" in product.vd: return None
dl = db.dicList("SELECT productid FROM object WHERE id=" + product.vd["objectid"])
if not dl:
msg = "Tried to get product ID from nonexisting objectid %s" % product.vd["objectid"]
errmsg(msg)
return None
return dl[0]["productid"]
def askAndWrapAlbum(ot, albumDir, productDirOnly, fileList):
"""If the album content is best represented as one document,
ask and concatenate files to one.
In this case return True, else False.
TODO: Do this later, as scheduled action.
"""
caption = "Checking if %s is one document" % productDirOnly
if ot not in ("MC", "LP"): return # One file per side. TODO: Later also include CDs.
exts = list(set([os.path.splitext(f)[1] for f in fileList])) # Including dots, e.g. set(['.mp3', '.jpg'])
if len(exts)>2: return 0 # Not what we are looking for.
audioExtList = [".mp3",".ogg",".wmv"] # TODO: make configurable.
imageExtList = [".jpg"] # TODO: make configurable.
# One ext must be of an audio type, the other, if existing, an image type.
audioExtIdx = 0 # Suppose the audio ext is the first, i.e. its index is 0.
if len(exts) == 1:
if exts[0] not in audioExtList: return 0
else: # Two exts. Find out which (index) is audio type.
if exts[0] not in audioExtList: audioExtIdx = 1
if exts[audioExtIdx] not in audioExtList: return 0
if exts[1-audioExtIdx] not in imageExtList: return 0
audioExt = exts[audioExtIdx]
audioFileList = [f for f in fileList if f.endswith(audioExt)]
print("audioFileList", audioFileList)
if len(audioFileList) != 2: return 0 # Exactly one file per side.
msg = textwrap.dedent("""
This directory: %s
seems to hold audio files: %s
that are best archived as one document.
Concatenate them and archive them as one document?
""")[1:-1] % (productDirOnly, ','.join(audioFileList))
if QMessageBox.question(None, caption, msg,
QMessageBox.StandardButtons(QMessageBox.No | QMessageBox.Yes),
QMessageBox.Yes) == QMessageBox.No: return 0
# Better use mp3wrap or so?
cmd = "cat '%s/%s' >>'%s/%s'" % (albumDir, audioFileList[1], albumDir, audioFileList[0])
print(cmd)
btc(cmd)
cmd = "rm '%s/%s'" % (albumDir, audioFileList[1])
print(cmd)
btc(cmd)
return 1
#now in product
def askAndRecordProductImage(ot, albumDir, productId):
"""If main directory contains image files (esp. jpg),
then for each image ask if it shows front cover, back cover or product (MC/disk) itself.
If so, register it as document and connect it with rel to product.
See document.downloadImage
"""
caption = "Checking product images"
#print ot, albumDir
fileList = getFileList(albumDir)
allExts = set([os.path.splitext(f)[1] for f in fileList]) # Including dots, e.g. set(['.mp3', '.jpg'])
supportedImageExtsList = [".jpg"] # TODO: make configurable.
imageExtSet = allExts.intersection(set(supportedImageExtsList))
#print "imageExtSet ", imageExtSet
if len(imageExtSet) != 1: return 0
imageExt = list(imageExtSet)[0]
imageFileList = [f for f in fileList if f.endswith(imageExt)]
print("imageFileList ", imageFileList)
# If user clicks Cancel on one of the images, the function aborts.
# If user clicks Skip on one of the images, only that image is skipped.
fileAnswers = {}
for (j0, file) in enumerate(imageFileList): # Loop through files sorted alphabetically.
(base,ext) = os.path.splitext(file)
j = j0 + 1
path = albumDir + '/' + file # Complete (source) file path.
print(path)
im = AlbumImageDialog(path)
if base == 'front': im.frontCoverButton.setFocus()
elif base == 'back': im.backCoverButton.setFocus()
if not im.exec_(): return # User clicked "Cancel".
fileAnswers[file] = im.answer
ta = archive.FileDbTransaction("Archiving cover and product image files")
for (file, answer) in fileAnswers.items():
if not answer: continue # Case when user clicked "Skip".
#print "reg. image file: %s, relid:%s" % (file, answer)
path = albumDir + '/' + file
# Archive this file as document and rel it to product.
doc = document.Document(src=path)
dupid = doc.checkDuplicate()
if dupid:
print("%s already known as doc %s" % (file, dupid))
continue
attributes = dict() # Will be filled by metadatareader.
metadatareader.readFromFile(attributes, path)
metadatareader.selectMaxLikelihood(attributes)
doc.getMetadataValues(attributes)
id = document.createDocumentRecord(doc, caption)
relDic = {"ent1":"5", "id1":id, "relid":answer, "ent2":"23", "id2":productId}
#print "relDic:", relDic
db.insertDic("rel", relDic)
def containsMetadata(path):
"""Return True if path is album (file or directory; not package) that contains metadata.
If not or in case of doubt, return False.
"""
# print "containsMetadata: path", path
if not path: return None
if os.path.isdir(path):
# print "containsMetadata: isdir"
if os.path.isfile(path + '/VIDEO_TS/VIDEO_TS.IFO'):
# print "isfile", path + '/VIDEO_TS/VIDEO_TS.IFO'
l = getFileList(path)
# print l
if l: return True
return False
#def getLevRatio(albumList):
def getLevRatio(docsToA):
"""Return minimum Levenshtein rate."""
levRatio = 1.0
oldTitle = None
# for album in albumList:
for i, album in docsToA.orig.iteritems():
for origDoc in album:
t = origDoc.vd["title"] if origDoc else ''
if oldTitle:
levRatio = min(levRatio, Levenshtein.ratio(oldTitle, t))
oldTitle = t
#print "oldTitle: %s, levRatio: %4.2f" % (oldTitle, levRatio)
return levRatio
def getItemDescr(h, item):
level = getText(item, h.levelColumn)
id = getText(item, h.idColumn) or "(no ID)"
file = getText(item, h.fileColumn) or "(no file)"
title = getText(item, h.dbTitleColumn) or "(no DB title)"
extTitle = "(ext. title: %s)" % (getText(item, h.extTitleColumn) or "none")
rawCnt = getText(item, h.rnColumn)
cnt = "#%s " % rawCnt if rawCnt else ''
# "document 12345: #01 Title (ext. title: none) [file: (no file)]
return "%s %s: %s%s %s [file: %s]" % (level, id, cnt, title, extTitle, file)
def getDescendantItems(h, topItem, level=None):
"""Return list of items below given topItem,
filtered by level if given."""
result = []
for item in traverseRecursive(topItem):
if level and getText(item, h.levelColumn)!=level: continue
result.append(item)
return result
#def getAncestorAlbumId(h, item):
#"""Return ID of given item if it is an album or of
#an ancestor if that is an album.
#"""
#while 1:
#if getText(item, h.levelColumn) == "album":
#return getText(item, h.idColumn)
#item = item.parent()
#if not item: return None
# print type(item)
# return None
#def getAncestorAlbumItem(h, item):
#"""Return given item if it is an album or
#the ancestor that is an album.
#"""
#while 1:
#if getText(item, h.levelColumn) == "album": return item
#item = item.parent()
#if not item: return None
def getProductRootDocId(productId):
sql = textwrap.dedent("""
SELECT d.id FROM product p, object o, cp, document d
WHERE o.productid=p.id AND cp.objectid=o.id AND cp.file='/'
AND cp.documentid=d.id and p.id=%s;
""") % productId
vl = db.valueList(sql)
return vl[0] if vl else None
#def getObjectProductList(objectId, productId):
#"""Get a list of tuples (cnt, objectId, productId)
#where the tuple with index 0 is the package (may be (0, None, None))
#and the following tuples represent albums.
#Input may be an album (object and/or product)
#or package product. At least one of objectId, productId must be not None.
#However, for each product only one album is returned.
#That album is objectId if it is given, otherwise an original, if there is.
#The list is ordered by object.descr.
#"""
#log = logging.getLogger("getObjectProductList")
#log.debug("objectId: %s, productId: %s" % (objectId, productId))
## Decide if it is a package or album.
## - no product ID given and none in DB -> album
## - product ID given and has prods -> package
## - product ID given and has packageId -> album
#if not productId:
#productId = db.readSql("SELECT productid FROM object WHERE id=%s" % objectId, 0, 1)
#sqlBase = "SELECT id FROM product p WHERE p.packageid=%s"
#prods = db.valueList(sqlBase % productId) if productId else None
#log.debug("prods: %s" % unicode(prods))
#if not prods: # Product is not package but still may be part of a package.
#if not productId: return [(0, None, None), (1, objectId, productId)]
#packageId = db.readSql("SELECT packageid FROM product WHERE id=%s" % productId, 0, 1)
#if not packageId: return [(0, None, None), (1, objectId, productId)] # Definitely no package.
#prods = db.valueList(sqlBase % packageId) # is part of package, now select package's parts
#packageObjectId = db.readSql("SELECT id FROM object WHERE productid=%s" % packageId, 0, 1)
#res = [(0, packageObjectId, packageId)]
#else: # productId is package
#res = [(0, objectId, productId)]
#objectList = []
#cnt = 0
#for productId in prods: # loop over package parts
#sql = "SELECT id, descr FROM object WHERE productid=%s" % productId
#sql += " ORDER BY (id=%s) DESC, orig DESC;" % (objectId or 0)
## ordering should ensure that given objectId is first, otherwise orig is first
#dl = db.dicList(sql)
#if dl: objectList.append((productId, dl[0]["id"], dl[0]["descr"]))
##print objectList
##print 90*'-'
## Sort objectList by descr which has index 2 in the tuples.
#objectList.sort(key=lambda x: x[2])
##print objectList
##print 90*'='
#for o in objectList:
#cnt += 1
## res.append((cnt, vl[0], productId))
#res.append((cnt, o[1], o[0]))
#return res
#def getObjectProductList(objectId, productId):
#"""Get a list of tuples (cnt, objectId, productId)
#where the tuple with index 0 is the package (may be (None, None))
#and the following tuples are albums.
#Input may be an album or package. At least one of objectId, productId must be not None.
#Ordering is by album, i.e. object.
#However, for each product only one album should be shown.
#That album should be objectId if it is given, otherwise an original, if there is.
#The list then must be ordered by object.descr.
#"""
#log = logging.getLogger("getObjectProductList")
#log.debug("objectId: %s, productId: %s" % (objectId, productId))
## Decide if it is a package or album.
## - no product ID given and none in DB -> album
## - product ID given and has prods -> package
## - product ID given and has packageId -> album
#if not productId:
#productId = db.readSql("SELECT productid FROM object WHERE id=%s" % objectId, 0, 1)
#sqlBase = textwrap.dedent("""
#SELECT o.id as oid, p.id as pid, o.descr FROM product p, object o
#WHERE p.packageid=%s AND o.productid=p.id ORDER BY convert_to(o.descr, 'utf8');
#""") # See below for ordering.
#prods = db.dicList(sqlBase % productId) if productId else None
#log.debug("prods: %s" % unicode(prods))
#if not prods: # Product is not package but still may be part of a package.
#if not productId: return [(0, None, None), (1, objectId, productId)]
#packageId = db.readSql("SELECT packageid FROM product WHERE id=%s" % productId, 0, 1)
#if not packageId: return [(0, None, None), (1, objectId, productId)] # Definitely no package.
#prods = db.dicList(sqlBase % packageId) # is part of package, now select package's parts
#packageObjectId = db.readSql("SELECT id FROM object WHERE productid=%s" % packageId, 0, 1)
#res = [(0, packageObjectId, packageId)]
#else: # productId is package
#res = [(0, objectId, productId)]
#for (cnt, product) in enumerate(prods): # loop over package parts
#res.append((cnt+1, product["oid"], product["pid"]))
#return res
def sumDigits(chk, start=0, end=1, step=2, mult=1):
return reduce(lambda x, y: int(x)+int(y), list(chk[start:end:step])) * mult
# http://www.postgresql.org/docs/current/static/isn.html
def eanCheckDigit(ean):
"""Return the checksum digit of an EAN-13/8 code.
TODO
"""
l = len(ean)
m0 = 2*(l%2)+1
m1 = 4-m0
print(m0, m1)
#if chk.isdigit() and __lenCheck(chk):
#if code == 'EAN13':
#m0=1
#m1=3
#elif code == 'EAN8':
#m0=3
#m1=1
#else:
#return None
# _len = codeLength[code]-1
t = 10 - (( sumDigits(chk, start=0, end=l-1, mult=m0) + \
sumDigits(chk, start=1, end=l-1, mult=m1) ) %10 ) %10
return t if t!=10 else 0
#if t == 10:
#return 0
#else:
#return t
#return None
# To include spaces etc. in ordering, order by convert_to(descr, 'utf8'),
# not by descr itself. Tip from:
# http://stackoverflow.com/questions/4955386/postgresql-ignores-dashes-when-ordering
# This is strange because charset is UTF8 already:
# # show lc_collate;
#lc_collate
#-------------
#en_US.UTF-8
# select descr from object where id>13659 order by descr;
# select descr from object where id>13659 order by convert_to(descr, 'utf8');
# Note that album objects may be in a certain order,
# but that order can be found nowhere in the data.
# It is simply the order of the discs in the package.
# Example: "Planet Erde" BBC documentation DVD package EAN 4006448756352
# which contains 6 DVDs the last one of which is named "Bonus Disc"
# and would come first by alphabetic ordering, but
# has the last place in the package, and by the
# convention of placing bonus discs last.
# In this case it may be useful to invent album names as follows:
# "Packagetitle DVD 1" .. "Packagetitle DVD 6"
# maybe extended by the names written on the discs:
# TODOs:
# After adding new role, take last role as default and select the new role
# Show (playback) parts if the whole is requested but not available, and vice versa
# Error message if Show external version and the wrong CD is in the drive
# Make sure MP3 files of whole albums can be split up in parts
# and part titles are conserved.
# Default-select "Add new"; automatically return new entity after AddNew
# For roles:
# after creating new role, select it
# provide default ord (last ord +1)
# provide default language (from document or from person)
# in FindDialog, don't require '+' for providing search string as default
# For persons, guess sex
"""
"Artist" is a generic term, encompasses pop singers,
classical voice types, etc.; it is suitable in role table,
but if more accurate information is available,
e.g. voice type, that should be used.
For (classical) voice type, the notion of that voice type
should be used, not the notion of the person with that type.
Note that classical opera singers can well sing in
(for them) foreign languages, e.g. German singers can sing
roles in Italian etc.; guessing a role's language from the
document is more accurate than from the person's language.
Where a person or a company needs to be provided,
both fields (personid and companyid) are present.
Examples are owner and role tables, but more such cases
may occur in the future. Both fields may be filled, then
this means "company ... under the direction of person ...".
12858 | chorus | a group of people assembled to sing together | 6890
2 | composer | person who writes (composes) music | 6880
58012 | tenor voice | the adult male singing voice above baritone | 6862
63811 | artist | someone who practises one of the fine arts, e.g. painter, or performs music vocally or instrumentally, etc. | 6861
63813 | orchestra | organized group of musicians | 6860
63886 | soprano | the highest female voice | 6859
39459 | mezzo-soprano | the female singing voice between contralto and soprano | 6834
58012 | tenor voice | the adult male singing voice above baritone | 6836
5939 | baritone voice | the second lowest adult male singing voice | 6850
6153 | basso | the lowest adult male singing voice | 6839
The role.ord field roughly gives the order of importance.
Note that operas on CDs are rarely complete, only highlights
are recorded. The overture, arias etc. of such an opera on CD
constitute a work of their own, which may be seen as a version
of the original opera composition. It thus inherits properties
from that composition (such as the composer, language etc.),
although it is not complete, i.e. not all parts of the composition
are represented in this work.
Take doc 77572: Fidelio as an example of such a work.
Here all roles are focused in the work document.
Another example is doc 77522: The Magic Flute, where
the single arias etc. are provided with roles information,
which is more accurate.
This information, together with some general text about the
composition (opera) and maybe the composer,
should replace a CD's supplement and back cover completely.
The goal of Unibas is not only to archive a CD completely,
but also the information on its packaging, supplement etc.
UnDVD handling:
dvdbackup
try genisoimage -gui -dvd-video -o "%s" "%s"' % (dest, src)
if successful:
archive result .iso file
else:
cat VTS_nn_i.VOB
with archive.catUndvd(undvd)
"""
class UnibasProcess():
"""A lengthy process (e.g. file copying)
the progress of which needs to be shown.
"""
def __init__(self, progressDialog, labelText):
self.progressDialog = progressDialog
self.labelText = labelText
# e = multiProgress.startFunction(cmd, dest) #"uhash('%s', widget)" % (archive.fullPath(objectid, file).replace("'", "\\'")), file)
class DestinationFileProcess(UnibasProcess):
"""A process that creates a file of (approximately) known size.
The command is executed, it is expected to create the file sizeFile.
That file is expected to grow up to endValue.
So the completion percentage is calculated as:
100 * size / endValue
where size is the actual size of the sizeFile.
Stdout is not read, but maybe stderr for error messages.
"""
def __init__(self, progressDialog, labelText, cmd, destPath, endValue, shell=False):
UnibasProcess.__init__(self, progressDialog, labelText)
self.command, self.destPath, self.endValue, self.shell = \
cmd, destPath, endValue, shell
def startCmd(self):#, command, sizeFile, endValue, shell=False):
"""Start a command and watch progress as file size."""
log = logging.getLogger("startCmd")
if log.isEnabledFor(logging.DEBUG):
if isinstance(self.command, list):
log.debug("command: %s" , ' '.join(self.command))
else:
log.debug("command: %s" , self.command)
self.process = subprocess.Popen(self.command, shell=self.shell)
# QCoreApplication.instance().processEvents() # get app for remaining responsive
progressDialog = self.progressDialog
progressDialog.setBottomWidgetLabelText(self.labelText)
progressDialog.setBottomWidgetProcess(self.process)
progressDialog.setBottomWidgetValue(0)
self.abort = False
while self.process.poll() is None:
QCoreApplication.instance().processEvents() # get app for remaining responsive
size = 0
if os.path.isdir(self.destPath):
for root, dirs, files in os.walk(self.destPath):
for file in files:
size += os.path.getsize(root + '/' + file)
elif os.path.isfile(self.destPath):
size = os.path.getsize(self.destPath)
if size > 0:
progressDialog.setBottomWidgetValue(100 * size / self.endValue)
time.sleep(1)
log.info("process no longer running")
progressDialog.setBottomWidgetValue(100)
self.exitCode = self.process.returncode
self.success = True
res = ''
if self.abort or self.exitCode:
self.success = False
res = "Process exited abnormally.\n"
if self.abort:
res += "Reason: Abort by user\n"
else:
res += "Exit code: " + unicode(self.exitCode) + "\n"
if self.exitCode and not self.abort:
log.error(res)
return res
class PercentageProcess(UnibasProcess):
"""A process that reads its progress information from stdout or stderr.
By default the percentage is read from stdout.
A default regexp is provided for this, however, it is recommended
to provide a regexp tailored for the situation.
Tested with HandBrakeCLI
TODO: Read stderr as well? What to do with it,
detect errors or remember it for later?
"""
def __init__(self, progressDialog, labelText, command,
outRegExpString="[^\d](\d+) ?%",
shell=False, bufsize=0):
UnibasProcess.__init__(self, progressDialog, labelText)
self.command, self.shell, self.bufsize = \
command, shell, bufsize
self.outRegExp = re.compile(outRegExpString) if outRegExpString else None
def startCmd(self):
"""Start a command and watch progress."""
log = logging.getLogger("startCmd")
if log.isEnabledFor(logging.DEBUG):
if isinstance(self.command, list):
log.debug("command: %s" , ' '.join(self.command))
else:
log.debug("command: %s" , self.command)
self.process = subprocess.Popen(self.command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=self.shell, bufsize=self.bufsize)
progressDialog = self.progressDialog
progressDialog.setBottomWidgetLabelText(self.labelText)
progressDialog.setBottomWidgetProcess(self.process)
progressDialog.setBottomWidgetValue(0)
self.abort = False
while self.process.poll() is None:
QCoreApplication.instance().processEvents() # get app for remaining responsive
self.readFromStdout()
# time.sleep(1)
log.info("process no longer running")
progressDialog.setBottomWidgetValue(100)
self.exitCode = self.process.returncode
self.success = True
res = ''
if self.abort or self.exitCode:
self.success = False
res = "Process exited abnormally.\n"
if self.abort:
res += "Reason: Abort by user\n"
else:
res += "Exit code: " + unicode(self.exitCode) + "\n"
if self.exitCode and not self.abort:
log.error(res)
return res
def readFromStdout(self):
# s = uc(self.process.readAllStandardOutput())
#print "before read()"
s = self.process.stdout.read(80)
ps = ''.join(c for c in s if ord(c)>=32)
#log.debug("From stdout: " + ps)
print "From stdout: " + ps
if self.outRegExp:
m = self.outRegExp.match(s) #,0)
if m:
print "matched"
# rawProgressValue = eval(self.extractExpr) if self.extractExpr else m.group(1)
rawProgressValue = m.group(1)
print "rawProgressValue ",rawProgressValue
#print "scaleValue: ", self.scaleValue
self.progressDialog.setBottomWidgetValue(int(rawProgressValue))
# self.progress.setValue(int(float(rawProgressValue) * self.scaleValue))
return
# if self.rememberStdout: self.outOutput += s