Initial import.
340
COPYING
Normal file
@@ -0,0 +1,340 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) 19yy <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) 19yy name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
4
data/compile.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
os.system("pyrcc4 picard.qrc -o ../picard/resources.py")
|
||||
BIN
data/images/CoverArtShadow.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
data/images/Picard16.png
Normal file
|
After Width: | Height: | Size: 485 B |
BIN
data/images/Picard32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
data/images/ToolbarAddDir.png
Normal file
|
After Width: | Height: | Size: 803 B |
BIN
data/images/ToolbarAddFiles.png
Normal file
|
After Width: | Height: | Size: 1001 B |
BIN
data/images/ToolbarAnalyze.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
data/images/ToolbarCluster.png
Normal file
|
After Width: | Height: | Size: 786 B |
BIN
data/images/ToolbarListen.png
Normal file
|
After Width: | Height: | Size: 978 B |
BIN
data/images/ToolbarLookup.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
data/images/ToolbarOptions.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
data/images/ToolbarRemove.png
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
data/images/ToolbarSave.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
data/images/ToolbarSubmit.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
data/images/analyze.png
Normal file
|
After Width: | Height: | Size: 723 B |
BIN
data/images/analyze.psd
Normal file
BIN
data/images/cd.png
Normal file
|
After Width: | Height: | Size: 931 B |
BIN
data/images/dir.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
data/images/file.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
data/images/note.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
data/images/reload.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
data/images/remove.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
data/images/remove.psd
Normal file
BIN
data/images/search.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
24
data/picard.qrc
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file>images/cd.png</file>
|
||||
<file>images/CoverArtShadow.png</file>
|
||||
<file>images/dir.png</file>
|
||||
<file>images/file.png</file>
|
||||
<file>images/note.png</file>
|
||||
<file>images/Picard16.png</file>
|
||||
<file>images/Picard32.png</file>
|
||||
<file>images/ToolbarAddDir.png</file>
|
||||
<file>images/ToolbarAddFiles.png</file>
|
||||
<file>images/ToolbarAnalyze.png</file>
|
||||
<file>images/ToolbarCluster.png</file>
|
||||
<file>images/ToolbarListen.png</file>
|
||||
<file>images/ToolbarLookup.png</file>
|
||||
<file>images/ToolbarOptions.png</file>
|
||||
<file>images/ToolbarRemove.png</file>
|
||||
<file>images/ToolbarSave.png</file>
|
||||
<file>images/search.png</file>
|
||||
<file>images/ToolbarSubmit.png</file>
|
||||
<file>images/remove.png</file>
|
||||
<file>images/analyze.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
7
docs/dnd.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Mime Types
|
||||
==========
|
||||
|
||||
* text/uri-list
|
||||
* application/picard.file-list
|
||||
* application/picard.album-list
|
||||
* application/picard.cluster-list
|
||||
21
docs/todo.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
TODO List
|
||||
=========
|
||||
|
||||
* More intelligent drag&drop, needs reimplementing dragMoveEvent
|
||||
* Convert AlbumTreeView from custom model/view to QTreeWidget (the Python
|
||||
model/view implementation is slow)
|
||||
* Save metadata from the "Server Metadata" box
|
||||
* Allow only one active selection. Either in the file view, or in the album view.
|
||||
* Removing albums
|
||||
* Reload album information
|
||||
* Matching files to tracks
|
||||
* Clustering
|
||||
* Matching cluster to album
|
||||
* Matching multiple files to album
|
||||
* Lookup file
|
||||
* Lookup cluster
|
||||
* Lookup album
|
||||
* Lookup track
|
||||
* CD lookup
|
||||
* Generate cuesheets from albums
|
||||
* Generate playlists
|
||||
1
picard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = '0.8.0'
|
||||
103
picard/album.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from threading import RLock
|
||||
from musicbrainz2.webservice import Query, WebServiceError, ReleaseIncludes
|
||||
from musicbrainz2.model import VARIOUS_ARTISTS_ID, NS_MMD_1
|
||||
from musicbrainz2.utils import extractUuid, extractFragment
|
||||
from picard.util import formatTime
|
||||
from picard.dataobj import DataObject
|
||||
from picard.track import Track
|
||||
from picard.artist import Artist
|
||||
|
||||
class AlbumLoadError(Exception):
|
||||
pass
|
||||
|
||||
class Album(DataObject):
|
||||
|
||||
def __init__(self, id, name, artist=None):
|
||||
DataObject.__init__(self, id, name)
|
||||
self._lock = RLock()
|
||||
self.unmatchedFiles = []
|
||||
self.artist = artist
|
||||
self.tracks = []
|
||||
self.duration = 0
|
||||
|
||||
def __str__(self):
|
||||
return u'<Album %s, name %s>' % (self.id, self.name)
|
||||
|
||||
def lock(self):
|
||||
self._lock.acquire()
|
||||
|
||||
def unlock(self):
|
||||
self._lock.release()
|
||||
|
||||
def load(self):
|
||||
self.tagger.log.debug("Loading album %r", self.id)
|
||||
|
||||
query = Query()
|
||||
release = None
|
||||
try:
|
||||
inc = ReleaseIncludes(artist=True, releaseEvents=True, discs=True, tracks=True)
|
||||
release = query.getReleaseById(self.id, inc)
|
||||
except WebServiceError, e:
|
||||
self.hasLoadError = True
|
||||
raise AlbumLoadError, e
|
||||
|
||||
self.lock()
|
||||
|
||||
self.name = release.title
|
||||
self.artist = Artist(release.artist.id, release.artist.name)
|
||||
|
||||
self.duration = 0
|
||||
self.tracks = []
|
||||
for track in release.tracks:
|
||||
if track.artist:
|
||||
artist = Artist(track.artist.id, track.artist.name)
|
||||
else:
|
||||
artist = Artist(release.artist.id, release.artist.name)
|
||||
tr = Track(track.id, track.title, artist, self)
|
||||
tr.duration = track.duration or 0
|
||||
self.tracks.append(tr)
|
||||
self.duration += tr.duration
|
||||
|
||||
self.unlock()
|
||||
|
||||
def getNumTracks(self):
|
||||
return len(self.tracks)
|
||||
|
||||
def addUnmatchedFile(self, file):
|
||||
self.unmatchedFiles.append(file)
|
||||
self.emit(QtCore.SIGNAL("fileAdded(int)"), file.id)
|
||||
|
||||
def getNumUnmatchedFiles(self):
|
||||
return len(self.unmatchedFiles)
|
||||
|
||||
numUnmatchedFiles = property(getNumUnmatchedFiles)
|
||||
|
||||
def removeFile(self, file):
|
||||
index = self.unmatchedFiles.index(file)
|
||||
self.emit(QtCore.SIGNAL("fileAboutToBeRemoved"), index)
|
||||
# self.test = self.unmatchedFiles[index]
|
||||
del self.unmatchedFiles[index]
|
||||
print self.unmatchedFiles
|
||||
self.emit(QtCore.SIGNAL("fileRemoved"), index)
|
||||
|
||||
67
picard/albummanager.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from picard.album import Album
|
||||
from picard.artist import Artist
|
||||
|
||||
class UnmatchedFiles(Album):
|
||||
|
||||
def __init__(self):
|
||||
self._origName = u"Unmatched Files (%d)"
|
||||
Album.__init__(self, u"[unmatched files]", self._origName)
|
||||
|
||||
def addUnmatchedFile(self, file):
|
||||
self.name = self._origName % (self.numUnmatchedFiles + 1)
|
||||
Album.addUnmatchedFile(self, file)
|
||||
|
||||
def removeFile(self, file):
|
||||
self.name = self._origName % (self.numUnmatchedFiles - 1)
|
||||
Album.removeFile(self, file)
|
||||
|
||||
|
||||
class Clusters(Album):
|
||||
pass
|
||||
|
||||
class AlbumManager(QtCore.QObject):
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.albums = []
|
||||
self.unmatchedFiles = UnmatchedFiles()
|
||||
self.clusters = Clusters(u"[clusters]", u"Clusters")
|
||||
|
||||
def load(self, albumId):
|
||||
albumId = unicode(albumId)
|
||||
album = Album(albumId, "[loading album information]", None)
|
||||
self.albums.append(album)
|
||||
self.emit(QtCore.SIGNAL("albumAdded"), album)
|
||||
self.tagger.worker.loadAlbum(album)
|
||||
#album.load()
|
||||
|
||||
def getAlbumById(self, id):
|
||||
for album in self.albums:
|
||||
if album.id == id:
|
||||
return album
|
||||
return None
|
||||
|
||||
def getNumAlbums(self):
|
||||
return len(self.albums)
|
||||
|
||||
32
picard/api.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from picard.component import Interface
|
||||
|
||||
class IFileOpener(Interface):
|
||||
|
||||
def getSupportedFormats(self):
|
||||
pass
|
||||
|
||||
def canOpen(self, fileName):
|
||||
pass
|
||||
|
||||
def open(self, fileName):
|
||||
pass
|
||||
|
||||
26
picard/artist.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from picard.dataobj import DataObject
|
||||
|
||||
class Artist(DataObject):
|
||||
|
||||
def __init__(self, id, name=None):
|
||||
DataObject.__init__(self, id, name)
|
||||
0
picard/browser/__init__.py
Normal file
122
picard/browser/browser.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
import urllib
|
||||
import httplib
|
||||
import BaseHTTPServer
|
||||
|
||||
class TaggerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
|
||||
def __init__(self,conn,addr,server):
|
||||
BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, conn, addr, server)
|
||||
|
||||
def do_POST(self):
|
||||
self.send_error(405, "POST not supported")
|
||||
|
||||
def do_HEAD(self):
|
||||
self.send_error(405, "HEAD not supported")
|
||||
|
||||
def do_GET(self):
|
||||
parsedArgs = {}
|
||||
[action, rest] = urllib.splitquery(self.path)
|
||||
if rest:
|
||||
args = rest.split('&');
|
||||
for kv in args:
|
||||
[key, value] = kv.split('=')
|
||||
parsedArgs[key] = unicode(value)
|
||||
|
||||
if action[0] == '/':
|
||||
action = action[1:]
|
||||
self.server.getBrowserIntegrationModule().action(action, parsedArgs)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write('<html><head><meta http-equiv="pragma" content="no-cache"></head><body>Nothing to see here!</body></html>\n')
|
||||
|
||||
def do_QUIT(self):
|
||||
self.server.getBrowserIntegrationModule().exitThread = True
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
class TaggerServer(BaseHTTPServer.HTTPServer, QtCore.QObject):
|
||||
|
||||
def __init__(self, addr, handlerClass):
|
||||
BaseHTTPServer.HTTPServer.__init__(self, addr, handlerClass)
|
||||
|
||||
def setBrowserIntegrationModule(self, bim):
|
||||
self.bim = bim
|
||||
|
||||
def getBrowserIntegrationModule(self):
|
||||
return self.bim
|
||||
|
||||
class BrowserIntegration(QtCore.QThread):
|
||||
|
||||
defaultPort = 8056
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.exitThread = False
|
||||
self.server = None
|
||||
|
||||
def start(self):
|
||||
self.log.debug("Starting the browser integration")
|
||||
QtCore.QThread.start(self)
|
||||
|
||||
def stop(self):
|
||||
self.log.debug("Stopping the browser integration")
|
||||
if self.isRunning():
|
||||
if self.port:
|
||||
conn = httplib.HTTPConnection("%s:%d" % self.server.server_address)
|
||||
conn.request("QUIT", "/")
|
||||
conn.getresponse()
|
||||
self.wait()
|
||||
|
||||
def action(self, action, args):
|
||||
self.log.debug("Browser integration event: action=%r, args=%r", action, args)
|
||||
if action == "init":
|
||||
self.emit(QtCore.SIGNAL("init(int)"), args)
|
||||
elif action == "openalbum":
|
||||
self.emit(QtCore.SIGNAL("loadAlbum(const QString &)"), args["id"])
|
||||
else:
|
||||
self.log.warning("Unknown browser integration event '%s'!", action)
|
||||
|
||||
def run(self):
|
||||
# Start the HTTP server
|
||||
port = self.defaultPort
|
||||
self.port = None
|
||||
while not self.port:
|
||||
try:
|
||||
self.server = TaggerServer(("127.0.0.1", port), TaggerRequestHandler)
|
||||
self.port = port
|
||||
except:
|
||||
port = port + 1
|
||||
|
||||
# Report the port number back to the main app
|
||||
self.action("init", self.port)
|
||||
|
||||
self.server.setBrowserIntegrationModule(self)
|
||||
while not self.exitThread:
|
||||
self.server.handle_request()
|
||||
|
||||
123
picard/browser/filelookup.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# Version: RCSL 1.0/RPSL 1.0/GPL 2.0
|
||||
#
|
||||
# Portions Copyright (c) 1995-2002 RealNetworks, Inc. All Rights Reserved.
|
||||
# Portions Copyright (c) 2004 Robert Kaye. All Rights Reserved.
|
||||
#
|
||||
# The contents of this file, and the files included with this file, are
|
||||
# subject to the current version of the RealNetworks Public Source License
|
||||
# Version 1.0 (the "RPSL") available at
|
||||
# http://www.helixcommunity.org/content/rpsl unless you have licensed
|
||||
# the file under the RealNetworks Community Source License Version 1.0
|
||||
# (the "RCSL") available at http://www.helixcommunity.org/content/rcsl,
|
||||
# in which case the RCSL will apply. You may also obtain the license terms
|
||||
# directly from RealNetworks. You may not use this file except in
|
||||
# compliance with the RPSL or, if you have a valid RCSL with RealNetworks
|
||||
# applicable to this file, the RCSL. Please see the applicable RPSL or
|
||||
# RCSL for the rights, obligations and limitations governing use of the
|
||||
# contents of the file.
|
||||
#
|
||||
# This file is part of the Helix DNA Technology. RealNetworks is the
|
||||
# developer of the Original Code and owns the copyrights in the portions
|
||||
# it created.
|
||||
#
|
||||
# This file, and the files included with this file, is distributed and made
|
||||
# available on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
|
||||
# EXPRESS OR IMPLIED, AND REALNETWORKS HEREBY DISCLAIMS ALL SUCH WARRANTIES,
|
||||
# INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
# FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
|
||||
#
|
||||
# Technology Compatibility Kit Test Suite(s) Location:
|
||||
# http://www.helixcommunity.org/content/tck
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
#
|
||||
# picard is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# picard is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with picard; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Contributor(s):
|
||||
# Robert Kaye
|
||||
#
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
import sys, urllib, webbrowser, os
|
||||
#from tunepimp import tunepimp, metadata
|
||||
#from tunepimp import track as tptrack
|
||||
#from picard import wpath
|
||||
import launch
|
||||
|
||||
class FileLookup(launch.Launch):
|
||||
|
||||
def __init__(self, parent, server, port, localPort):
|
||||
launch.Launch.__init__(self, parent)
|
||||
self.server = server
|
||||
self.localPort = int(localPort)
|
||||
self.port = port
|
||||
|
||||
def _encode(self, text):
|
||||
return urllib.quote(text.encode('UTF-8', 'replace'))
|
||||
|
||||
def discLookup(self, url):
|
||||
return self.launch("%s&tport=%d" % (url, self.localPort))
|
||||
|
||||
def _lookup(self, type_, id_):
|
||||
url = "http://%s:%d/%s/%s.html?tport=%d" % (
|
||||
self._encode(self.server),
|
||||
self.port,
|
||||
type_,
|
||||
id_,
|
||||
self.localPort)
|
||||
return self.launch(url)
|
||||
|
||||
def trackLookup(self, trackId):
|
||||
return self._lookup('track', trackId)
|
||||
|
||||
def albumLookup(self, albumId):
|
||||
return self._lookup('album', albumId)
|
||||
|
||||
def artistLookup(self, artistId):
|
||||
return self._lookup('artist', artistId)
|
||||
|
||||
def _search(self, type_, query):
|
||||
url = "http://%s:%d/search/textsearch.html?limit=25&type=%s&query=%s&tport=%d" % (
|
||||
self._encode(self.server),
|
||||
self.port,
|
||||
type_,
|
||||
self._encode(query),
|
||||
self.localPort)
|
||||
return self.launch(url)
|
||||
|
||||
def artistSearch(self, query):
|
||||
return self._search('artist', query)
|
||||
|
||||
def albumSearch(self, query):
|
||||
return self._search('release', query)
|
||||
|
||||
def trackSearch(self, query):
|
||||
return self._search('track', query)
|
||||
|
||||
def tagLookup(self, artist, release, track, trackNum, duration, fileName, puid):
|
||||
url = "http://%s:%d/taglookup.html?tport=%d&artist=%s&release=%s&track=%s&tracknum=%s&duration=%s&filename=%s&puid=%s" % (
|
||||
self._encode(self.server),
|
||||
self.port,
|
||||
self.localPort,
|
||||
self._encode(artist),
|
||||
self._encode(release),
|
||||
self._encode(track),
|
||||
trackNum,
|
||||
duration,
|
||||
self._encode(wpath.wpath().basename(fileName)),
|
||||
self._encode(puid))
|
||||
return self.launch(url)
|
||||
117
picard/browser/launch.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# Version: RCSL 1.0/RPSL 1.0/GPL 2.0
|
||||
#
|
||||
# Portions Copyright (c) 1995-2002 RealNetworks, Inc. All Rights Reserved.
|
||||
# Portions Copyright (c) 2004 Robert Kaye. All Rights Reserved.
|
||||
#
|
||||
# The contents of this file, and the files included with this file, are
|
||||
# subject to the current version of the RealNetworks Public Source License
|
||||
# Version 1.0 (the "RPSL") available at
|
||||
# http://www.helixcommunity.org/content/rpsl unless you have licensed
|
||||
# the file under the RealNetworks Community Source License Version 1.0
|
||||
# (the "RCSL") available at http://www.helixcommunity.org/content/rcsl,
|
||||
# in which case the RCSL will apply. You may also obtain the license terms
|
||||
# directly from RealNetworks. You may not use this file except in
|
||||
# compliance with the RPSL or, if you have a valid RCSL with RealNetworks
|
||||
# applicable to this file, the RCSL. Please see the applicable RPSL or
|
||||
# RCSL for the rights, obligations and limitations governing use of the
|
||||
# contents of the file.
|
||||
#
|
||||
# This file is part of the Helix DNA Technology. RealNetworks is the
|
||||
# developer of the Original Code and owns the copyrights in the portions
|
||||
# it created.
|
||||
#
|
||||
# This file, and the files included with this file, is distributed and made
|
||||
# available on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
|
||||
# EXPRESS OR IMPLIED, AND REALNETWORKS HEREBY DISCLAIMS ALL SUCH WARRANTIES,
|
||||
# INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
# FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
|
||||
#
|
||||
# Technology Compatibility Kit Test Suite(s) Location:
|
||||
# http://www.helixcommunity.org/content/tck
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
#
|
||||
# picard is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# picard is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with picard; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Contributor(s):
|
||||
# Robert Kaye
|
||||
# Lukas Lalinsky
|
||||
#
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
import sys, os, webbrowser, tempfile
|
||||
#from picard import wpath
|
||||
|
||||
# KDE default browser
|
||||
if 'KDE_FULL_SESSION' in os.environ and os.environ['KDE_FULL_SESSION'] == 'true' and webbrowser._iscommand('kfmclient'):
|
||||
webbrowser.register('kfmclient', None, webbrowser.GenericBrowser("kfmclient exec '%s' &"))
|
||||
if 'BROWSER' in os.environ:
|
||||
webbrowser._tryorder.insert(len(os.environ['BROWSER'].split(os.pathsep)), 'kfmclient')
|
||||
else:
|
||||
webbrowser._tryorder.insert(0, 'kfmclient')
|
||||
|
||||
# GNOME default browser
|
||||
if 'GNOME_DESKTOP_SESSION_ID' in os.environ and webbrowser._iscommand('gnome-open'):
|
||||
webbrowser.register('gnome-open', None, webbrowser.GenericBrowser("gnome-open '%s' &"))
|
||||
if 'BROWSER' in os.environ:
|
||||
webbrowser._tryorder.insert(len(os.environ['BROWSER'].split(os.pathsep)), 'gnome-open')
|
||||
else:
|
||||
webbrowser._tryorder.insert(0, 'gnome-open')
|
||||
|
||||
class Launch(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
def getTempFile(self):
|
||||
tempDir = tempfile.gettempdir()
|
||||
return wpath.wpath().join(tempDir, "post.html")
|
||||
|
||||
def cleanup(self):
|
||||
try:
|
||||
os.unlink(self.getTempFile())
|
||||
except:
|
||||
pass
|
||||
|
||||
def launch(self, url):
|
||||
# If the browser var does not specify the %s, warn the user
|
||||
browser = os.environ.get('BROWSER')
|
||||
if browser and browser not in webbrowser._browsers and ('%s' not in browser or '&' not in browser):
|
||||
dlg = wx.MessageDialog(self.parent, "Your BROWSER variable does not contain a %s and/or a & ."+
|
||||
" To ensure that your browser launches correctly and doesn't lock the rest of the "+
|
||||
" application, make sure your BROWSER environment varable includes a %s &. For example, "+
|
||||
' BROWSER="firefox \'%s\' &" should work to launch Firefox correctly.', style=wx.OK)
|
||||
dlg.ShowModal()
|
||||
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def post(self, post):
|
||||
try:
|
||||
file = open(self.getTempFile(), "w")
|
||||
file.write(post);
|
||||
file.close()
|
||||
except IOError:
|
||||
dlg = wx.MessageDialog(self.parent, "Could not write a temporary file to launch a browser.",
|
||||
"HTTP POST Launch", style=wx.OK)
|
||||
dlg.ShowModal()
|
||||
|
||||
self.launch("file://" + self.getTempFile())
|
||||
293
picard/component.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
#
|
||||
# Copyright (C) 2003-2006 Edgewall Software
|
||||
# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
|
||||
# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# 3. The name of the author may not be used to endorse or promote
|
||||
# products derived from this software without specific prior
|
||||
# written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS
|
||||
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
# Author: Jonas Borgström <jonas@edgewall.com>
|
||||
# Christopher Lenz <cmlenz@gmx.de>
|
||||
|
||||
__all__ = ['Component', 'ExtensionPoint', 'implements', 'Interface']
|
||||
|
||||
from PyQt4 import QtCore
|
||||
import sip
|
||||
|
||||
import inspect, types, __builtin__
|
||||
|
||||
############## preliminary: two utility functions #####################
|
||||
|
||||
def skip_redundant(iterable, skipset=None):
|
||||
"Redundant items are repeated items or items in the original skipset."
|
||||
if skipset is None: skipset = set()
|
||||
for item in iterable:
|
||||
if item not in skipset:
|
||||
skipset.add(item)
|
||||
yield item
|
||||
|
||||
|
||||
def remove_redundant(metaclasses):
|
||||
skipset = set([types.ClassType])
|
||||
for meta in metaclasses: # determines the metaclasses to be skipped
|
||||
skipset.update(inspect.getmro(meta)[1:])
|
||||
return tuple(skip_redundant(metaclasses, skipset))
|
||||
|
||||
##################################################################
|
||||
## now the core of the module: two mutually recursive functions ##
|
||||
##################################################################
|
||||
|
||||
memoized_metaclasses_map = {}
|
||||
|
||||
def get_noconflict_metaclass(bases, left_metas, right_metas):
|
||||
"""Not intended to be used outside of this module, unless you know
|
||||
what you are doing."""
|
||||
# make tuple of needed metaclasses in specified priority order
|
||||
metas = left_metas + tuple(map(type, bases)) + right_metas
|
||||
needed_metas = remove_redundant(metas)
|
||||
|
||||
# return existing confict-solving meta, if any
|
||||
if needed_metas in memoized_metaclasses_map:
|
||||
return memoized_metaclasses_map[needed_metas]
|
||||
# nope: compute, memoize and return needed conflict-solving meta
|
||||
elif not needed_metas: # wee, a trivial case, happy us
|
||||
meta = type
|
||||
elif len(needed_metas) == 1: # another trivial case
|
||||
meta = needed_metas[0]
|
||||
# check for recursion, can happen i.e. for Zope ExtensionClasses
|
||||
elif needed_metas == bases:
|
||||
raise TypeError("Incompatible root metatypes", needed_metas)
|
||||
else: # gotta work ...
|
||||
metaname = '_' + ''.join([m.__name__ for m in needed_metas])
|
||||
meta = classmaker()(metaname, needed_metas, {})
|
||||
memoized_metaclasses_map[needed_metas] = meta
|
||||
return meta
|
||||
|
||||
def classmaker(left_metas=(), right_metas=()):
|
||||
def make_class(name, bases, adict):
|
||||
metaclass = get_noconflict_metaclass(bases, left_metas, right_metas)
|
||||
return metaclass(name, bases, adict)
|
||||
return make_class
|
||||
|
||||
class Interface(object):
|
||||
"""Marker base class for extension point interfaces."""
|
||||
|
||||
|
||||
class ExtensionPoint(property):
|
||||
"""Marker class for extension points in components."""
|
||||
|
||||
def __init__(self, interface):
|
||||
"""Create the extension point.
|
||||
|
||||
@param interface: the `Interface` subclass that defines the protocol
|
||||
for the extension point
|
||||
"""
|
||||
property.__init__(self, self.extensions)
|
||||
self.interface = interface
|
||||
self.__doc__ = 'List of components that implement `%s`' % \
|
||||
self.interface.__name__
|
||||
|
||||
def extensions(self, component):
|
||||
"""Return a list of components that declare to implement the extension
|
||||
point interface."""
|
||||
extensions = ComponentMeta._registry.get(self.interface, [])
|
||||
return filter(None, [component.compmgr[cls] for cls in extensions])
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a textual representation of the extension point."""
|
||||
return '<ExtensionPoint %s>' % self.interface.__name__
|
||||
|
||||
|
||||
class ComponentMeta(type):
|
||||
"""Meta class for components.
|
||||
|
||||
Takes care of component and extension point registration.
|
||||
"""
|
||||
_components = []
|
||||
_registry = {}
|
||||
|
||||
def __new__(cls, name, bases, d):
|
||||
"""Create the component class."""
|
||||
|
||||
new_class = type.__new__(cls, name, bases, d)
|
||||
if name == 'Component':
|
||||
# Don't put the Component base class in the registry
|
||||
return new_class
|
||||
|
||||
# Only override __init__ for Components not inheriting ComponentManager
|
||||
if True not in [issubclass(x, ComponentManager) for x in bases]:
|
||||
# Allow components to have a no-argument initializer so that
|
||||
# they don't need to worry about accepting the component manager
|
||||
# as argument and invoking the super-class initializer
|
||||
init = d.get('__init__')
|
||||
if not init:
|
||||
# Because we're replacing the initializer, we need to make sure
|
||||
# that any inherited initializers are also called.
|
||||
for init in [b.__init__._original for b in new_class.mro()
|
||||
if issubclass(b, Component)
|
||||
and '__init__' in b.__dict__]:
|
||||
break
|
||||
def maybe_init(self, compmgr, init=init, cls=new_class):
|
||||
if cls not in compmgr.components:
|
||||
compmgr.components[cls] = self
|
||||
if init:
|
||||
init(self)
|
||||
maybe_init._original = init
|
||||
new_class.__init__ = maybe_init
|
||||
|
||||
if d.get('abstract'):
|
||||
# Don't put abstract component classes in the registry
|
||||
return new_class
|
||||
|
||||
ComponentMeta._components.append(new_class)
|
||||
for interface in d.get('_implements', []):
|
||||
ComponentMeta._registry.setdefault(interface, []).append(new_class)
|
||||
for base in [base for base in bases if hasattr(base, '_implements')]:
|
||||
for interface in base._implements:
|
||||
ComponentMeta._registry.setdefault(interface, []).append(new_class)
|
||||
|
||||
return new_class
|
||||
|
||||
|
||||
class QComponentMeta(ComponentMeta, sip.wrappertype):
|
||||
"""Wrapper metaclass to aviod metaclass conflict.
|
||||
"""
|
||||
pass
|
||||
|
||||
def implements(*interfaces):
|
||||
"""
|
||||
Can be used in the class definiton of `Component` subclasses to declare
|
||||
the extension points that are extended.
|
||||
"""
|
||||
import sys
|
||||
|
||||
frame = sys._getframe(1)
|
||||
locals = frame.f_locals
|
||||
|
||||
# Some sanity checks
|
||||
assert locals is not frame.f_globals and '__module__' in frame.f_locals, \
|
||||
'implements() can only be used in a class definition'
|
||||
assert not '_implements' in locals, \
|
||||
'implements() can only be used once in a class definition'
|
||||
|
||||
locals['_implements'] = interfaces
|
||||
|
||||
|
||||
class Component(QtCore.QObject):
|
||||
"""Base class for components.
|
||||
|
||||
Every component can declare what extension points it provides, as well as
|
||||
what extension points of other components it extends.
|
||||
"""
|
||||
__metaclass__ = QComponentMeta
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""Return an existing instance of the component if it has already been
|
||||
activated, otherwise create a new instance.
|
||||
"""
|
||||
# If this component is also the component manager, just invoke that
|
||||
if issubclass(cls, ComponentManager):
|
||||
self = super(Component, cls).__new__(cls)
|
||||
self.compmgr = self
|
||||
return self
|
||||
|
||||
# The normal case where the component is not also the component manager
|
||||
compmgr = args[0]
|
||||
self = compmgr.components.get(cls)
|
||||
if self is None:
|
||||
self = super(Component, cls).__new__(cls)
|
||||
self.compmgr = compmgr
|
||||
compmgr.component_activated(self)
|
||||
return self
|
||||
|
||||
|
||||
class ComponentManager(object):
|
||||
"""The component manager keeps a pool of active components."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the component manager."""
|
||||
self.components = {}
|
||||
self.enabled = {}
|
||||
if isinstance(self, Component):
|
||||
self.components[self.__class__] = self
|
||||
|
||||
def __contains__(self, cls):
|
||||
"""Return wether the given class is in the list of active components."""
|
||||
return cls in self.components
|
||||
|
||||
def __getitem__(self, cls):
|
||||
"""Activate the component instance for the given class, or return the
|
||||
existing the instance if the component has already been activated."""
|
||||
if cls not in self.enabled:
|
||||
self.enabled[cls] = self.is_component_enabled(cls)
|
||||
if not self.enabled[cls]:
|
||||
return None
|
||||
component = self.components.get(cls)
|
||||
if not component:
|
||||
if cls not in ComponentMeta._components:
|
||||
raise TracError, 'Component "%s" not registered' % cls.__name__
|
||||
try:
|
||||
component = cls(self)
|
||||
except TypeError, e:
|
||||
raise TracError, 'Unable to instantiate component %r (%s)' \
|
||||
% (cls, e)
|
||||
return component
|
||||
|
||||
def component_activated(self, component):
|
||||
"""Can be overridden by sub-classes so that special initialization for
|
||||
components can be provided.
|
||||
"""
|
||||
|
||||
def is_component_enabled(self, cls):
|
||||
"""Can be overridden by sub-classes to veto the activation of a
|
||||
component.
|
||||
|
||||
If this method returns False, the component with the given class will
|
||||
not be available.
|
||||
"""
|
||||
return True
|
||||
|
||||
90
picard/config.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
|
||||
defaultConfig = {
|
||||
u"persist/viewCoverArt": True,
|
||||
u"persist/viewFileBrowser": False,
|
||||
u"persist/windowGeometry": QtCore.QRect(10, 10, 780, 680),
|
||||
u"persist/windowMaximized": False,
|
||||
}
|
||||
|
||||
class ConfigError(Exception):
|
||||
pass
|
||||
|
||||
class ConfigGroup(object):
|
||||
|
||||
def __init__(self, config, name):
|
||||
self.config = config
|
||||
self.name = name
|
||||
|
||||
def get(self, name, default=QtCore.QVariant()):
|
||||
key = "%s/%s" % (self.name, name)
|
||||
if self.config.contains(key):
|
||||
return self.config.value(key)
|
||||
else:
|
||||
return default
|
||||
|
||||
def getString(self, name, default=None):
|
||||
key = "%s/%s" % (self.name, name)
|
||||
if self.config.contains(key):
|
||||
return unicode(self.config.value(key).toString())
|
||||
else:
|
||||
return default
|
||||
|
||||
def getInt(self, name, default=None):
|
||||
key = "%s/%s" % (self.name, name)
|
||||
if self.config.contains(key):
|
||||
value, ok = self.config.value(key).toInt()
|
||||
if ok:
|
||||
return value
|
||||
return default
|
||||
|
||||
def getBool(self, name, default=None):
|
||||
key = "%s/%s" % (self.name, name)
|
||||
if self.config.contains(key):
|
||||
return self.config.value(key).toBool()
|
||||
else:
|
||||
return default
|
||||
|
||||
def set(self, name, value):
|
||||
key = "%s/%s" % (self.name, name)
|
||||
self.config.setValue(key, QtCore.QVariant(value))
|
||||
|
||||
class Config(QtCore.QSettings):
|
||||
|
||||
organization = u"MusicBrainz"
|
||||
application = u"MusicBrainz Picard 1.0"
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes the configuration."""
|
||||
QtCore.QSettings.__init__(self, self.organization, self.application)
|
||||
self.setting = ConfigGroup(self, u"setting")
|
||||
self.persist = ConfigGroup(self, u"persist")
|
||||
self.profile = ConfigGroup(self, u"profile/default")
|
||||
|
||||
def switchProfile(self, profileName):
|
||||
"""Sets the current profile."""
|
||||
key = u"profile/%s" % (profileName,)
|
||||
if self.contains(key):
|
||||
self.profile.name = key
|
||||
else:
|
||||
raise ConfigError, "Unknown profile '%s'" % (profileName,)
|
||||
45
picard/dataobj.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
|
||||
class DataObject(QtCore.QObject):
|
||||
|
||||
def __init__(self, id, name):
|
||||
QtCore.QObject.__init__(self)
|
||||
self._id = id
|
||||
self._name = name
|
||||
|
||||
def setId(self, id):
|
||||
self._id = id
|
||||
|
||||
def getId(self):
|
||||
return self._id
|
||||
|
||||
id = property(getId, setId)
|
||||
|
||||
def getName(self):
|
||||
return self._name
|
||||
|
||||
def setName(self, name):
|
||||
self._name = name
|
||||
|
||||
name = property(getName, setName)
|
||||
|
||||
|
||||
112
picard/file.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
import os.path
|
||||
from picard.metadata import Metadata
|
||||
|
||||
class AudioProperties(object):
|
||||
|
||||
def __init__(self):
|
||||
self.length = 0
|
||||
self.bitrate = 0
|
||||
|
||||
class File(QtCore.QObject):
|
||||
|
||||
_idCounter = 1
|
||||
|
||||
def __init__(self, fileName):
|
||||
QtCore.QObject.__init__(self)
|
||||
assert(isinstance(fileName, unicode))
|
||||
self._lock = QtCore.QMutex()
|
||||
self._id = File._idCounter
|
||||
File._idCounter += 1
|
||||
self.fileName = fileName
|
||||
self.baseFileName = os.path.basename(fileName)
|
||||
self.album = None
|
||||
|
||||
self.localMetadata = Metadata()
|
||||
self.serverMetadata = Metadata()
|
||||
self.audioProperties = AudioProperties()
|
||||
|
||||
def lock(self):
|
||||
self._lock.lock()
|
||||
|
||||
def unlock(self):
|
||||
self._lock.unlock()
|
||||
|
||||
def getId(self):
|
||||
return self._id
|
||||
|
||||
id = property(getId)
|
||||
|
||||
def save(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def getNewMetadata(self):
|
||||
return self.serverMetadata
|
||||
|
||||
def moveToAlbumAsUnlinked(self, album):
|
||||
"""Moves the file to a given album as 'unmatched'."""
|
||||
self.removeFromAlbum()
|
||||
self.log.debug("File #%d moving to album %s as unlinked", self.getId(), album.getId())
|
||||
self.album = album
|
||||
self.album.addUnmatchedFile(self)
|
||||
|
||||
def removeFromAlbum(self):
|
||||
"""Removes the file from whatever album it may be on. Does nothing if
|
||||
the file is not currently on an album."""
|
||||
if self.album is None:
|
||||
return
|
||||
self.log.debug("File #%d being removed from album %s", self.getId(), self.album.getId())
|
||||
self.album.removeFile(self)
|
||||
self.album = None
|
||||
|
||||
|
||||
class FileManager(QtCore.QObject):
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.connect(self, QtCore.SIGNAL("fileAdded(int)"), self.onFileAdded)
|
||||
self.mutex = QtCore.QMutex()
|
||||
self.files = {}
|
||||
|
||||
def getFile(self, fileId):
|
||||
locker = QtCore.QMutexLocker(self.mutex)
|
||||
return self.files[fileId]
|
||||
|
||||
def addFile(self, file):
|
||||
self.log.debug("Adding file %s", str(file));
|
||||
self.mutex.lock()
|
||||
self.files[file.id] = file
|
||||
self.mutex.unlock()
|
||||
self.emit(QtCore.SIGNAL("fileAdded(int)"), file.id)
|
||||
|
||||
def onFileAdded(self, fileId):
|
||||
file = self.getFile(fileId)
|
||||
file.moveToAlbumAsUnlinked(self.tagger.albumManager.unmatchedFiles)
|
||||
|
||||
def removeFiles(self, files):
|
||||
for file in files:
|
||||
self.mutex.lock()
|
||||
file.removeFromAlbum()
|
||||
del self.files[file.id]
|
||||
self.mutex.unlock()
|
||||
|
||||
81
picard/metadata.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from copy import copy
|
||||
|
||||
class Metadata(QtCore.QObject):
|
||||
|
||||
"""Class to handle tag lists.
|
||||
|
||||
@see http://wiki.musicbrainz.org/UnifiedTagging
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
self.tags = {}
|
||||
|
||||
def compare(self, other):
|
||||
return 0.0
|
||||
|
||||
def copy(self, other):
|
||||
self.tags = copy(other.tags)
|
||||
|
||||
def set(self, name, value):
|
||||
self.tags[name.upper()] = value
|
||||
|
||||
def get(self, name, default=u""):
|
||||
name = name.upper()
|
||||
if self.tags.has_key(name):
|
||||
return self.tags[name]
|
||||
return default
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self.set(name, value)
|
||||
|
||||
def __contains__(self, item):
|
||||
self.tags.has_key(item)
|
||||
|
||||
def getTitle(self):
|
||||
return self["TITLE"]
|
||||
|
||||
def setTitle(self, value):
|
||||
self["TITLE"] = value
|
||||
|
||||
title = property(getTitle, setTitle)
|
||||
|
||||
def getArtist(self):
|
||||
return self["ARTIST"]
|
||||
|
||||
def setArtist(self, value):
|
||||
self["ARTIST"] = value
|
||||
|
||||
artist = property(getArtist, setArtist)
|
||||
|
||||
def getAlbum(self):
|
||||
return self["ALBUM"]
|
||||
|
||||
def setAlbum(self, value):
|
||||
self["ALBUM"] = value
|
||||
|
||||
album = property(getAlbum, setAlbum)
|
||||
|
||||
88
picard/parsefilename.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from picard.metadata import Metadata
|
||||
import re
|
||||
|
||||
_patterns = [
|
||||
# AlbumArtist/1999 - Album/01-TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)((?P<year>\d{4}) - )(?P<album>.*)(/|\\)(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist - Album/01 - TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist - Album/01-TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist - Album/01. TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2})\. (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist - Album/01 TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2}) (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist - Album/01_Artist_-_TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2})_(?P<artist>.*)_-_(?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# Album/Artist - Album - 01 - TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P=artist) - (?P<album>.*) - (?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/Artist - 01 - TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<artist>.*) - (?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/01. Artist - TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2})\. (?P<artist>.*) - (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/01 - Artist - TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2}) - (?P<artist>.*) - (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/01 - TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/01. TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2})\. (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/01 TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2}) (?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/Album-01-TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P=album)-(?P<tracknum>\d{2})-(?P<artist>.*)-(?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/Album-01-Artist-TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P=album)-(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"),
|
||||
# AlbumArtist/Album/Artist-01-TrackTitle.ext
|
||||
re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<artist>.*)-(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"),
|
||||
]
|
||||
|
||||
def parseFileName(fileName):
|
||||
for pattern in _patterns:
|
||||
match = pattern.match(fileName)
|
||||
if match:
|
||||
mdata = Metadata()
|
||||
#mdata.artist = match.group("albumartist")
|
||||
mdata.artist = match.group("artist")
|
||||
mdata.title = match.group("title")
|
||||
mdata.album = match.group("album")
|
||||
|
||||
return mdata
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Thanks to folks at http://www.last.fm/group/Get%2BYour%2BDamn%2BTags%2BRight/forum/13179/_/99927 :)
|
||||
testCases = [
|
||||
(u"F:\\Hudba\\2 Unlimited\\No Limit\\01 No Limit.mp3", u"2 Unlimited", u"No Limit", u"No Limit"),
|
||||
(u"F:\\Hudba\\2 Unlimited\\No Limit\\No Limit-01-No Limit.mp3", u"2 Unlimited", u"No Limit", u"No Limit"),
|
||||
(u"F:\\Hudba\\2 Unlimited\\No Limit\\No Limit-01-Test-No Limit.mp3", u"Test", u"No Limit", u"No Limit"),
|
||||
(u"F:\\grooves\\Brian Eno - Another Green World (1975)\\08 - Sombre Reptiles.ogg", u"Brian Eno", u"Another Green World (1975)", u"Sombre Reptiles"),
|
||||
(u"My Documents/Music/Various Artists/Album/01 - Artist - Track.ogg", u"Artist", u"Album", u"Track"),
|
||||
(u"M:\\Albums\\Artist\\Album\\artist - 01 - title.mp3", u"artist", u"Album", u"title"),
|
||||
(u"F:\\artist\\(year) album\\01 - title.mp3", u"artist", u"(year) album", u"title"),
|
||||
(u"/home/blaster/Data/Audio/Music/Deep Purple/[2003] Bananas/01 - Deep Purple - House of Pain.ogg", u"Deep Purple", u"[2003] Bananas", u"House of Pain"),
|
||||
(u"\\A\\A Perfect Circle\\(2000) Mer De Noms\\01 - The Hollow.mp3", u"A Perfect Circle", u"(2000) Mer De Noms", u"The Hollow"),
|
||||
(u"..\\My Music\\Metal\\Sonata Arctica\\Sonata Arctica - Successor - 05 - Shy.mp3", u"Sonata Arctica", u"Successor", u"Shy"),
|
||||
(u"D:\\Music\\Artist - Album (year)\\01_artist_-_trackname.mp3", u"artist", u"Album (year)", u"trackname"),
|
||||
(u"root/MP3/Band/Album/band-01-name.mp3", u"band", u"Album", u"name"),
|
||||
#(u"D:\\Music\\accPlus 64kb\\Artist\\Year - Album\\00 - Title - Artist.acc", u"Artist", u"Year - Album", u"Title"),
|
||||
(u"C:\\My Documents\\Media\\Audio\\A\\Autolux\\Future Perfect\\01. Turnstile Blues.mp3", u"Autolux", u"Future Perfect", u"Turnstile Blues"),
|
||||
(u"music\\artist\\1999 - Album Name\\01-TrackName.mp3", u"artist", u"Album Name", u"TrackName"),
|
||||
]
|
||||
ok = 0
|
||||
for testCase in testCases:
|
||||
mdata = parseFileName(testCase[0])
|
||||
print testCase[0]
|
||||
if not mdata:
|
||||
print "Error"
|
||||
else:
|
||||
if mdata.artist != testCase[1]:
|
||||
print "Error", "-%s-" % mdata.artist, "-%s-" % testCase[1]
|
||||
elif mdata.album != testCase[2]:
|
||||
print "Error", "-%s-" % mdata.album, "-%s-" % testCase[2]
|
||||
elif mdata.title != testCase[3]:
|
||||
print "Error", "-%s-" % mdata.title, "-%s-" % testCase[3]
|
||||
else:
|
||||
ok += 1
|
||||
print "OK"
|
||||
print len(testCases), ok
|
||||
|
||||
0
picard/plugins/__init__.py
Normal file
187
picard/plugins/cuesheet.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import re
|
||||
from picard.api import IFileOpener
|
||||
from picard.component import Component, implements
|
||||
from picard.file import File
|
||||
|
||||
_whitespace_re = re.compile('\s', re.UNICODE)
|
||||
_split_re = re.compile('\s*("[^"]*"|[^ ]+)\s*', re.UNICODE)
|
||||
|
||||
def msfToMs(msf):
|
||||
msf = msf.split(":")
|
||||
return ((int(msf[0]) * 60 + int(msf[1])) * 75 + int(msf[2])) * 1000 / 75
|
||||
|
||||
class CuesheetTrack(list):
|
||||
|
||||
def __init__(self, cuesheet, index):
|
||||
list.__init__(self)
|
||||
self.cuesheet = cuesheet
|
||||
self.index = index
|
||||
|
||||
def find(self, prefix):
|
||||
return [i for i in self if tuple(i[:len(prefix)]) == tuple(prefix)]
|
||||
|
||||
def getTrackNumber(self):
|
||||
return self.index
|
||||
|
||||
def getLength(self):
|
||||
try:
|
||||
nextTrack = self.cuesheet.tracks[self.index+1]
|
||||
index0 = self.find((u"INDEX",u"01"))
|
||||
index1 = nextTrack.find((u"INDEX",u"01"))
|
||||
return msfToMs(index1[0][2]) - msfToMs(index0[0][2])
|
||||
except IndexError:
|
||||
return 0
|
||||
|
||||
def getField(self, prefix):
|
||||
try:
|
||||
return self.find(prefix)[0][len(prefix)]
|
||||
except IndexError:
|
||||
return u""
|
||||
|
||||
def getArtist(self):
|
||||
return self.getField((u"PERFORMER",))
|
||||
|
||||
def getTitle(self):
|
||||
return self.getField((u"TITLE",))
|
||||
|
||||
def setArtist(self, artist):
|
||||
found = False
|
||||
for item in self:
|
||||
if item[0] == u"PERFORMER":
|
||||
if not found:
|
||||
item[1] = artist
|
||||
found = True
|
||||
else:
|
||||
del item
|
||||
if not found:
|
||||
self.append((u"PERFORMER", artist))
|
||||
|
||||
artist = property(getArtist, setArtist)
|
||||
|
||||
class Cuesheet(object):
|
||||
|
||||
def __init__(self, fileName):
|
||||
self.fileName = fileName
|
||||
self.tracks = []
|
||||
|
||||
def read(self):
|
||||
f = file(self.fileName)
|
||||
self.parse(f.readlines())
|
||||
f.close()
|
||||
|
||||
def unquote(self, string):
|
||||
if string.startswith('"'):
|
||||
if string.endswith('"'):
|
||||
return string[1:-1]
|
||||
else:
|
||||
return string[1:]
|
||||
return string
|
||||
|
||||
def quote(self, string):
|
||||
if _whitespace_re.search(string):
|
||||
return '"' + string.replace('"', '\'') + '"'
|
||||
return string
|
||||
|
||||
def parse(self, lines):
|
||||
track = CuesheetTrack(self, 0)
|
||||
self.tracks = [track]
|
||||
isUnicode = False
|
||||
for line in lines:
|
||||
# remove BOM
|
||||
if line.startswith('\xfe\xff'):
|
||||
isUnicode = True
|
||||
line = line[1:]
|
||||
# decode to unicode string
|
||||
line = line.strip()
|
||||
if isUnicode:
|
||||
line = line.decode('UTF-8', 'replace')
|
||||
else:
|
||||
line = line.decode('ISO-8859-1', 'replace')
|
||||
# parse the line
|
||||
split = [self.unquote(s) for s in _split_re.findall(line)]
|
||||
keyword = split[0].upper()
|
||||
if keyword == 'TRACK':
|
||||
trackNum = int(split[1])
|
||||
track = CuesheetTrack(self, trackNum)
|
||||
self.tracks.append(track)
|
||||
track.append(split)
|
||||
|
||||
def write(self):
|
||||
lines = []
|
||||
for num, track in self.tracks.items():
|
||||
for line in track:
|
||||
indent = 0
|
||||
if num > 0:
|
||||
if line[0] == "TRACK":
|
||||
indent = 2
|
||||
elif line[0] != "FILE":
|
||||
indent = 4
|
||||
line2 = u" ".join([self.quote(s) for s in line])
|
||||
lines.append(" " * indent + line2.encode("UTF-8"))
|
||||
return "\r\n".join(lines)
|
||||
|
||||
class CuesheetVirtualFile(File):
|
||||
|
||||
def __init__(self, cuesheet, track):
|
||||
File.__init__(self, cuesheet.fileName)
|
||||
self.cuesheet = cuesheet
|
||||
self.track = track
|
||||
self.localMetadata["ARTIST"] = track.getArtist()
|
||||
self.localMetadata["TITLE"] = track.getTitle()
|
||||
self.localMetadata["ALBUM"] = cuesheet.tracks[0].getTitle()
|
||||
self.localMetadata["ALBUMARTIST"] = cuesheet.tracks[0].getArtist()
|
||||
self.localMetadata["TRACKNUMBER"] = str(track.getTrackNumber())
|
||||
self.localMetadata["TOTALTRACKS"] = str(len(cuesheet.tracks) - 1)
|
||||
self.serverMetadata.copy(self.localMetadata)
|
||||
self.audioProperties.length = track.getLength()
|
||||
|
||||
class CuesheetOpener(Component):
|
||||
|
||||
implements(IFileOpener)
|
||||
|
||||
def getSupportedFormats(self):
|
||||
return ((u".cue", u"Cuesheet"),)
|
||||
|
||||
def canOpenFile(self, fileName):
|
||||
return fileName[-4:].lower() == u".cue"
|
||||
|
||||
def openFile(self, fileName):
|
||||
cuesheet = Cuesheet(fileName)
|
||||
cuesheet.read()
|
||||
files = []
|
||||
for track in cuesheet.tracks[1:]:
|
||||
file = CuesheetVirtualFile(cuesheet, track)
|
||||
files.append(file)
|
||||
print files
|
||||
return files
|
||||
|
||||
if __name__ == "__main__":
|
||||
cue = Cuesheet("a.cue")
|
||||
cue.read()
|
||||
for num, track in cue.tracks.items():
|
||||
print num, track
|
||||
|
||||
print "-------"
|
||||
print cue.write()
|
||||
# cue.tracks[0].setArtist(0)
|
||||
|
||||
|
||||
59
picard/plugins/mutagenmp3.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from picard.component import *
|
||||
from picard.api import IFileOpener
|
||||
from picard.file import File
|
||||
from picard.util import encodeFileName
|
||||
import logging
|
||||
|
||||
class MutagenMp3File(File):
|
||||
|
||||
def read(self):
|
||||
import mutagen.mp3
|
||||
mfile = mutagen.mp3.MP3(encodeFileName(self.fileName))
|
||||
|
||||
# Local metadata
|
||||
if mfile.has_key('TIT2'):
|
||||
self.localMetadata.title = unicode(mfile['TIT2'])
|
||||
if mfile.has_key('TPE1'):
|
||||
self.localMetadata.artist = unicode(mfile['TPE1'])
|
||||
if mfile.has_key('TALB'):
|
||||
self.localMetadata.album = unicode(mfile['TALB'])
|
||||
|
||||
self.serverMetadata.copy(self.localMetadata)
|
||||
|
||||
# Audio properties
|
||||
self.audioProperties.length = int(mfile.info.length * 1000)
|
||||
self.audioProperties.bitrate = mfile.info.bitrate / 1000.0
|
||||
|
||||
def save(self):
|
||||
import mutagen.mp3
|
||||
mp3File = mutagen.mp3.MP3(encodeFileName(self.fileName))
|
||||
mp3File.save()
|
||||
|
||||
|
||||
class MutagenMp3Component(Component):
|
||||
|
||||
implements(IFileOpener)
|
||||
|
||||
# IFileOpener
|
||||
|
||||
supportedFormats = {
|
||||
u".mp3": (MutagenMp3File, u"MPEG Layer-3"),
|
||||
}
|
||||
|
||||
def getSupportedFormats(self):
|
||||
return [(key, value[1]) for key, value in self.supportedFormats.items()]
|
||||
|
||||
def canOpenFile(self, fileName):
|
||||
for ext in self.supportedFormats.keys():
|
||||
if fileName.endswith(ext):
|
||||
return True
|
||||
return False
|
||||
|
||||
def openFile(self, fileName):
|
||||
for ext in self.supportedFormats.keys():
|
||||
if fileName.endswith(ext):
|
||||
file = self.supportedFormats[ext][0](fileName)
|
||||
file.read()
|
||||
return (file,)
|
||||
return None
|
||||
|
||||
2469
picard/resources.py
Normal file
169
picard/tagger.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
import gettext
|
||||
import locale
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
import picard.resources
|
||||
|
||||
from picard.albummanager import AlbumManager
|
||||
from picard.api import IFileOpener
|
||||
from picard.browser.filelookup import FileLookup
|
||||
from picard.browser.browser import BrowserIntegration
|
||||
from picard.component import ComponentManager, Interface, ExtensionPoint, Component
|
||||
from picard.config import Config
|
||||
from picard.ui.mainwindow import MainWindow
|
||||
from picard.worker import WorkerThread
|
||||
from picard.file import FileManager
|
||||
|
||||
# Install gettext "noop" function.
|
||||
import __builtin__
|
||||
__builtin__.__dict__['N_'] = lambda a: a
|
||||
|
||||
class Tagger(QtGui.QApplication, ComponentManager, Component):
|
||||
|
||||
fileOpeners = ExtensionPoint(IFileOpener)
|
||||
|
||||
def __init__(self, localeDir):
|
||||
QtGui.QApplication.__init__(self, sys.argv)
|
||||
ComponentManager.__init__(self)
|
||||
|
||||
self.config = Config()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
# format='%(message)s',
|
||||
format='%(asctime)s %(levelname)-8s %(pathname)s#%(lineno)d [%(thread)04d]\n%(message)s',
|
||||
datefmt='%H:%M:%S')
|
||||
self.log = logging.getLogger('picard')
|
||||
|
||||
QtCore.QObject.tagger = self
|
||||
QtCore.QObject.config = self.config
|
||||
QtCore.QObject.log = self.log
|
||||
|
||||
self.setupGettext(localeDir)
|
||||
self.loadComponents()
|
||||
|
||||
self.worker = WorkerThread()
|
||||
self.connect(self.worker, QtCore.SIGNAL("addFiles(const QStringList &)"), self.onAddFiles)
|
||||
|
||||
self.browserIntegration = BrowserIntegration()
|
||||
|
||||
self.fileManager = FileManager()
|
||||
self.albumManager = AlbumManager()
|
||||
|
||||
self.connect(self.browserIntegration, QtCore.SIGNAL("loadAlbum(const QString &)"), self.albumManager.load)
|
||||
|
||||
self.window = MainWindow()
|
||||
self.connect(self.window, QtCore.SIGNAL("addFiles"), self.onAddFiles)
|
||||
self.connect(self.window, QtCore.SIGNAL("addDirectory"), self.onAddDirectory)
|
||||
self.connect(self.worker, QtCore.SIGNAL("statusBarMessage(const QString &)"), self.window.setStatusBarMessage)
|
||||
self.connect(self.window, QtCore.SIGNAL("search"), self.onSearch)
|
||||
self.connect(self.window, QtCore.SIGNAL("lookup"), self.onLookup)
|
||||
|
||||
self.worker.start()
|
||||
self.browserIntegration.start()
|
||||
|
||||
def exit(self):
|
||||
self.browserIntegration.stop()
|
||||
self.worker.stop()
|
||||
|
||||
def run(self):
|
||||
self.window.show()
|
||||
res = self.exec_()
|
||||
self.exit()
|
||||
return res
|
||||
|
||||
def setupGettext(self, localeDir):
|
||||
"""Setup locales, load translations, install gettext functions."""
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, os.environ["LANG"])
|
||||
except KeyError:
|
||||
os.environ["LANG"] = locale.getdefaultlocale()[0]
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.log.debug("Loading gettext translation, localeDir=%r", localeDir)
|
||||
self.translation = gettext.translation("picard", localeDir)
|
||||
self.translation.install(True)
|
||||
except IOError, e:
|
||||
__builtin__.__dict__['_'] = lambda a: a
|
||||
self.log.warning(e)
|
||||
|
||||
def loadComponents(self):
|
||||
# Load default components
|
||||
default_components = (
|
||||
'picard.plugins.mutagenmp3',
|
||||
'picard.plugins.cuesheet',
|
||||
)
|
||||
for module in default_components:
|
||||
__import__(module)
|
||||
|
||||
def getSupportedFormats(self):
|
||||
"""Returns list of supported formats.
|
||||
|
||||
Format:
|
||||
[('.mp3', 'MPEG Layer-3 File'), ('.cue', 'Cuesheet'), ...]
|
||||
"""
|
||||
formats = []
|
||||
for opener in self.fileOpeners:
|
||||
formats.extend(opener.getSupportedFormats())
|
||||
return formats
|
||||
|
||||
def onAddFiles(self, files):
|
||||
files = [os.path.normpath(unicode(a)) for a in files]
|
||||
self.log.debug("onAddFiles(%r)", files)
|
||||
for fileName in files:
|
||||
for opener in self.fileOpeners:
|
||||
if opener.canOpenFile(fileName):
|
||||
self.worker.readFile(fileName, opener.openFile)
|
||||
|
||||
def onAddDirectory(self, directory):
|
||||
directory = os.path.normpath(directory)
|
||||
self.log.debug("onAddDirectory(%r)", directory)
|
||||
self.worker.readDirectory(directory)
|
||||
|
||||
def onSearch(self, text, type_):
|
||||
lookup = FileLookup(self, "musicbrainz.org", 80, self.browserIntegration.port)
|
||||
getattr(lookup, type_ + "Search")(text)
|
||||
|
||||
def onLookup(self, metadata):
|
||||
pass
|
||||
|
||||
def saveFiles(self, files):
|
||||
for file in files:
|
||||
self.worker.saveFile(file)
|
||||
|
||||
def main(localeDir=None):
|
||||
tagger = Tagger(localeDir)
|
||||
sys.exit(tagger.run())
|
||||
|
||||
44
picard/track.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from picard.util import formatTime
|
||||
from picard.dataobj import DataObject
|
||||
|
||||
class Track(DataObject):
|
||||
|
||||
def __init__(self, id, name, artist=None, album=None):
|
||||
DataObject.__init__(self, id, name)
|
||||
self.artist = artist
|
||||
self.album = album
|
||||
self.duration = 0
|
||||
|
||||
def __str__(self):
|
||||
return u"<Track %s, name %s>" % (self.id, self.name)
|
||||
|
||||
def getDuration(self):
|
||||
return self._duration
|
||||
|
||||
def setDuration(self, duration):
|
||||
self._duration = duration
|
||||
self._durationStr = formatTime(duration)
|
||||
|
||||
duration = property(getDuration, setDuration)
|
||||
|
||||
0
picard/ui/__init__.py
Normal file
61
picard/ui/coverartbox.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
class CoverArtBox(QtGui.QGroupBox):
|
||||
|
||||
def __init__(self, parent):
|
||||
QtGui.QGroupBox.__init__(self, _("Cover Art"))
|
||||
self.setupUi()
|
||||
|
||||
def test(self):
|
||||
self.emit(QtCore.SIGNAL("TestSignal"), 1, 4)
|
||||
|
||||
def setupUi(self):
|
||||
self.layout = QtGui.QVBoxLayout()
|
||||
|
||||
#cover = QtGui.QPixmap("cover.jpg")
|
||||
#cover = cover.scaled(105, 105, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation);
|
||||
|
||||
img = QtGui.QPixmap(":/images/CoverArtShadow.png")
|
||||
#painter = QtGui.QPainter(img)
|
||||
#painter.drawPixmap(1,1,cover)
|
||||
#painter.end()
|
||||
|
||||
self.coverArt = QtGui.QLabel()
|
||||
self.coverArt.setPixmap(img)
|
||||
self.coverArt.setAlignment(QtCore.Qt.AlignTop)
|
||||
|
||||
#amazonLayout = QtGui.QHBoxLayout()
|
||||
|
||||
#self.amazonBuyLabel = QtGui.QLabel('<a href="http://www.amazon.com/">Buy</a> | <a href="http://www.amazon.com/">Info</a>')
|
||||
#self.amazonBuyLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
#self.amazonInfoLabel = QtGui.QLabel('')
|
||||
#self.amazonInfoLabel.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft)
|
||||
|
||||
#amazonLayout.addWidget(self.amazonBuyLabel)
|
||||
#amazonLayout.addWidget(self.amazonInfoLabel)
|
||||
|
||||
self.layout.addWidget(self.coverArt, 0)
|
||||
#self.layout.addWidget(self.amazonBuyLabel, 1)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
314
picard/ui/itemviews.py
Normal file
@@ -0,0 +1,314 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
import os
|
||||
from picard.album import Album
|
||||
from picard.file import File
|
||||
from picard.albummanager import UnmatchedFiles
|
||||
from picard.util import formatTime, encodeFileName
|
||||
from picard.ui.tageditor import TagEditor
|
||||
|
||||
__all__ = ["FileTreeView", "AlbumTreeView"]
|
||||
|
||||
class BaseTreeView(QtGui.QTreeWidget):
|
||||
|
||||
def __init__(self, mainWindow, parent):
|
||||
QtGui.QTreeWidget.__init__(self, parent)
|
||||
self.mainWindow = mainWindow
|
||||
|
||||
self.numHeaderSections = 3
|
||||
self.defaultSectionSizes = (250, 40, 100, 100)
|
||||
self.setHeaderLabels([_(u"Title"), _(u"Time"), _(u"Artist")])
|
||||
self.restoreState()
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
self.setDragEnabled(True)
|
||||
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
self.objectToItem = {}
|
||||
self.itemToObject = {}
|
||||
|
||||
def restoreState(self):
|
||||
name = "header" + self.__class__.__name__
|
||||
header = self.header()
|
||||
for i in range(self.numHeaderSections):
|
||||
size = self.config.persist.getInt("%s%d" % (name, i), \
|
||||
self.defaultSectionSizes[i])
|
||||
header.resizeSection(i, size)
|
||||
|
||||
def saveState(self):
|
||||
name = "header" + self.__class__.__name__
|
||||
for i in range(self.numHeaderSections):
|
||||
size = self.header().sectionSize(i)
|
||||
self.config.persist.set("%s%d" % (name, i), size)
|
||||
|
||||
def registerObject(self, obj, item):
|
||||
self.objectToItem[obj] = item
|
||||
self.itemToObject[item] = obj
|
||||
|
||||
def unregisterObject(self, obj, item):
|
||||
del self.objectToItem[obj]
|
||||
del self.itemToObject[item]
|
||||
|
||||
def getObjectFromItem(self, item):
|
||||
return self.itemToObject[item]
|
||||
|
||||
def getItemFromObject(self, obj):
|
||||
return self.objectToItem[obj]
|
||||
|
||||
def supportedDropActions(self):
|
||||
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
|
||||
|
||||
def mimeTypes(self):
|
||||
"""List of MIME types accepted by this view."""
|
||||
return ["text/uri-list", "application/picard.file", "application/picard.album"]
|
||||
|
||||
def startDrag(self, supportedActions):
|
||||
"""Start drag, *without* using pixmap."""
|
||||
items = self.selectedItems()
|
||||
if items:
|
||||
drag = QtGui.QDrag(self)
|
||||
drag.setMimeData(self.mimeData(items))
|
||||
if drag.start(supportedActions) == QtCore.Qt.MoveAction:
|
||||
self.log.debug("MoveAction")
|
||||
|
||||
def selectedObjects(self):
|
||||
items = self.selectedItems()
|
||||
return [self.itemToObject[item] for item in items]
|
||||
|
||||
|
||||
class FileTreeView(BaseTreeView):
|
||||
|
||||
def __init__(self, mainWindow, parent):
|
||||
BaseTreeView.__init__(self, mainWindow, parent)
|
||||
|
||||
|
||||
|
||||
# Create the context menu
|
||||
|
||||
self.editTagsAct = QtGui.QAction(_("Edit &Tags..."), self)
|
||||
self.connect(self.editTagsAct, QtCore.SIGNAL("triggered()"), self.editTags)
|
||||
|
||||
self.lookupAct = QtGui.QAction(QtGui.QIcon(":/images/search.png"), _("&Lookup"), self)
|
||||
|
||||
self.analyzeAct = QtGui.QAction(QtGui.QIcon(":/images/analyze.png"), _("&Analyze"), self)
|
||||
|
||||
self.contextMenu = QtGui.QMenu(self)
|
||||
self.contextMenu.addAction(self.editTagsAct)
|
||||
self.contextMenu.addSeparator()
|
||||
self.contextMenu.addAction(self.lookupAct)
|
||||
self.contextMenu.addAction(self.analyzeAct)
|
||||
self.contextMenu.addAction(self.mainWindow.removeAct)
|
||||
|
||||
# Prepare some common icons
|
||||
|
||||
self.dirIcon = QtGui.QIcon(":/images/dir.png")
|
||||
self.fileIcon = QtGui.QIcon(":/images/file.png")
|
||||
|
||||
# "Unmatched Files"
|
||||
|
||||
self.unmatchedFilesItem = QtGui.QTreeWidgetItem()
|
||||
self.unmatchedFilesItem.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
self.unmatchedFilesItem.setText(0, "Unmatched Files")
|
||||
self.unmatchedFilesItem.setIcon(0, self.dirIcon)
|
||||
self.addTopLevelItem(self.unmatchedFilesItem)
|
||||
self.objectToItem[self.tagger.albumManager.unmatchedFiles] = self.unmatchedFilesItem
|
||||
self.itemToObject[self.unmatchedFilesItem] = self.tagger.albumManager.unmatchedFiles
|
||||
|
||||
unmatched = self.tagger.albumManager.unmatchedFiles
|
||||
self.connect(unmatched, QtCore.SIGNAL("fileAdded(int)"), self.addUnmatchedFile)
|
||||
self.connect(unmatched, QtCore.SIGNAL("fileAboutToBeRemoved"), self.beginRemoveFile)
|
||||
self.connect(unmatched, QtCore.SIGNAL("fileRemoved"), self.endRemoveFile)
|
||||
|
||||
self.fileGroupsItem = QtGui.QTreeWidgetItem()
|
||||
self.fileGroupsItem.setFlags(QtCore.Qt.ItemIsEnabled)
|
||||
self.fileGroupsItem.setText(0, "Track Groups")
|
||||
self.fileGroupsItem.setIcon(0, self.dirIcon)
|
||||
self.addTopLevelItem(self.fileGroupsItem)
|
||||
|
||||
#self.connect(self, QtCore.SIGNAL("itemSelectionChanged()"), self.updateSelection)
|
||||
self.connect(self, QtCore.SIGNAL("doubleClicked(QModelIndex)"), self.handleDoubleClick)
|
||||
|
||||
|
||||
def addUnmatchedFile(self, fileId):
|
||||
unmatchedFiles = self.tagger.albumManager.unmatchedFiles
|
||||
file = self.tagger.fileManager.getFile(fileId)
|
||||
fileItem = QtGui.QTreeWidgetItem()
|
||||
fileItem.setIcon(0, self.fileIcon)
|
||||
fileItem.setText(0, file.localMetadata.get("TITLE", ""))
|
||||
fileItem.setText(1, formatTime(file.audioProperties.length))
|
||||
fileItem.setText(2, file.localMetadata.get("ARTIST", ""))
|
||||
self.unmatchedFilesItem.addChild(fileItem)
|
||||
|
||||
self.objectToItem[file] = fileItem
|
||||
self.itemToObject[fileItem] = file
|
||||
|
||||
# Update title for pseudo-album "Unmatched Tracks"
|
||||
self.unmatchedFilesItem.setText(0, unmatchedFiles.name)
|
||||
|
||||
|
||||
# self.emit(QtCore.SIGNAL("rowsInserted(const QModelIndex &, int, int)"),
|
||||
# self.createIndex(0, 0, self.tagger.albumManager.unmatchedFiles),
|
||||
# 0, 0)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
items = self.selectedItems()
|
||||
|
||||
canEditTags = False
|
||||
canLookup = False
|
||||
canAnalyze = False
|
||||
canRemove = False
|
||||
|
||||
if len(items) == 1:
|
||||
canEditTags = True
|
||||
canLookup = True
|
||||
|
||||
if len(items) > 0:
|
||||
#canAnalyze = True
|
||||
canRemove = True
|
||||
|
||||
self.editTagsAct.setEnabled(canEditTags)
|
||||
self.lookupAct.setEnabled(canLookup)
|
||||
self.analyzeAct.setEnabled(canAnalyze)
|
||||
#self.removeAct.setEnabled(canRemove)
|
||||
|
||||
self.contextMenu.popup(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
def removeFiles(self):
|
||||
files = self.selectedObjects()
|
||||
self.tagger.fileManager.removeFiles(files)
|
||||
|
||||
def beginRemoveFile(self, row):
|
||||
file = self.tagger.albumManager.unmatchedFiles.unmatchedFiles[row]
|
||||
item = self.objectToItem[file]
|
||||
index = self.unmatchedFilesItem.indexOfChild(item)
|
||||
self.unmatchedFilesItem.takeChild(index)
|
||||
del self.objectToItem[file]
|
||||
del self.itemToObject[item]
|
||||
|
||||
def endRemoveFile(self, row):
|
||||
# Update title for pseudo-album "Unmatched Tracks"
|
||||
unmatchedFiles = self.tagger.albumManager.unmatchedFiles
|
||||
self.unmatchedFilesItem.setText(0, unmatchedFiles.name)
|
||||
|
||||
def openTagEditor(self, obj):
|
||||
tagEditor = TagEditor(obj.getNewMetadata(), self)
|
||||
tagEditor.exec_()
|
||||
self.emit(QtCore.SIGNAL("selectionChanged"), [obj])
|
||||
|
||||
def editTags(self):
|
||||
objects = self.selectedObjects()
|
||||
self.openTagEditor(objects[0])
|
||||
|
||||
def handleDoubleClick(self, index):
|
||||
obj = self.itemToObject[self.itemFromIndex(index)]
|
||||
if isinstance(obj, File):
|
||||
self.openTagEditor(obj)
|
||||
|
||||
# Drag & drop
|
||||
|
||||
def dropMimeData(self, parent, index, data, action):
|
||||
"""Handle drop."""
|
||||
print "dropMimeType"
|
||||
print data
|
||||
print [unicode(i) for i in data.formats()]
|
||||
files = []
|
||||
uriList = data.urls()
|
||||
for uri in uriList:
|
||||
print uri.scheme()
|
||||
print uri.host()
|
||||
if uri.scheme() == "file":
|
||||
fileName = str(uri.toLocalFile())
|
||||
fileName = unicode(QtCore.QUrl.fromPercentEncoding(QtCore.QByteArray(fileName)))
|
||||
if os.path.isdir(encodeFileName(fileName)):
|
||||
self.emit(QtCore.SIGNAL("addDirectory"), fileName)
|
||||
else:
|
||||
files.append(fileName)
|
||||
print files
|
||||
self.emit(QtCore.SIGNAL("addFiles"), files)
|
||||
return True
|
||||
|
||||
def mimeData(self, items):
|
||||
"""Return MIME data for specified items."""
|
||||
fileIds = []
|
||||
for item in items:
|
||||
obj = self.itemToObject[item]
|
||||
fileIds.append(str(obj.getId()))
|
||||
mimeData = QtCore.QMimeData()
|
||||
mimeData.setData("application/picard.file", "\n".join(fileIds))
|
||||
return mimeData
|
||||
|
||||
|
||||
class AlbumTreeView(BaseTreeView):
|
||||
|
||||
def __init__(self, mainWindow, parent):
|
||||
BaseTreeView.__init__(self, mainWindow, parent)
|
||||
|
||||
self.cdIcon = QtGui.QIcon(":/images/cd.png")
|
||||
self.noteIcon = QtGui.QIcon(":/images/note.png")
|
||||
|
||||
self.connect(self.tagger.albumManager, QtCore.SIGNAL("albumAdded"),
|
||||
self.addAlbum)
|
||||
self.connect(self.tagger.worker, QtCore.SIGNAL("albumLoaded(QString)"),
|
||||
self.updateAlbum)
|
||||
|
||||
def addAlbum(self, album):
|
||||
item = QtGui.QTreeWidgetItem()
|
||||
item.setText(0, album.name)
|
||||
item.setIcon(0, self.cdIcon)
|
||||
font = item.font(0)
|
||||
font.setBold(True)
|
||||
for i in range(3):
|
||||
item.setFont(i, font)
|
||||
self.registerObject(album, item)
|
||||
self.addTopLevelItem(item)
|
||||
|
||||
def updateAlbum(self, albumId):
|
||||
self.log.debug("updateAlbum, %s", albumId)
|
||||
album = self.tagger.albumManager.getAlbumById(unicode(albumId))
|
||||
albumItem = self.getItemFromObject(album)
|
||||
albumItem.setText(0, album.name)
|
||||
albumItem.setText(1, formatTime(album.duration))
|
||||
albumItem.setText(2, album.artist.name)
|
||||
i = 1
|
||||
for track in album.tracks:
|
||||
item = QtGui.QTreeWidgetItem()
|
||||
item.setText(0, "%d. %s" % (i, track.name))
|
||||
item.setIcon(0, self.noteIcon)
|
||||
item.setText(1, formatTime(track.duration))
|
||||
item.setText(2, track.artist.name)
|
||||
self.registerObject(track, item)
|
||||
albumItem.addChild(item)
|
||||
i += 1
|
||||
|
||||
def mimeData(self, items):
|
||||
"""Return MIME data for specified items."""
|
||||
albumIds = []
|
||||
for item in items:
|
||||
obj = self.getObjectFromItem(item)
|
||||
if isinstance(obj, Album):
|
||||
albumIds.append(str(obj.getId()))
|
||||
#elif isinstance(obj, Track):
|
||||
# trackIds.append(str(obj.getId()))
|
||||
mimeData = QtCore.QMimeData()
|
||||
mimeData.setData("application/picard.album", "\n".join(albumIds))
|
||||
return mimeData
|
||||
|
||||
381
picard/ui/mainwindow.py
Normal file
@@ -0,0 +1,381 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from picard.file import File
|
||||
from picard.util import decodeFileName, encodeFileName
|
||||
from picard.ui.coverartbox import CoverArtBox
|
||||
from picard.ui.metadatabox import MetadataBox
|
||||
from picard.ui.itemviews import FileTreeView, AlbumTreeView
|
||||
|
||||
class MainWindow(QtGui.QMainWindow):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QMainWindow.__init__(self, parent)
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
self.setWindowTitle(_("MusicBrainz Picard"))
|
||||
icon = QtGui.QIcon()
|
||||
icon.addFile(":/images/Picard16.png")
|
||||
icon.addFile(":/images/Picard32.png")
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.createActions()
|
||||
self.createMenus()
|
||||
self.createStatusBar()
|
||||
self.createToolBar()
|
||||
|
||||
centralWidget = QtGui.QWidget(self)
|
||||
self.setCentralWidget(centralWidget)
|
||||
|
||||
self.splitter = QtGui.QSplitter(centralWidget)
|
||||
|
||||
self.fileTreeView = FileTreeView(self, self.splitter)
|
||||
self.connect(self.fileTreeView, QtCore.SIGNAL("itemSelectionChanged()"), self.updateFileTreeSelection)
|
||||
self.connect(self.fileTreeView, QtCore.SIGNAL("addFiles"), QtCore.SIGNAL("addFiles"))
|
||||
self.connect(self.fileTreeView, QtCore.SIGNAL("addDirectory"), QtCore.SIGNAL("addDirectory"))
|
||||
|
||||
self.albumTreeView = AlbumTreeView(self, self.splitter)
|
||||
self.connect(self.albumTreeView, QtCore.SIGNAL("itemSelectionChanged()"), self.updateAlbumTreeSelection)
|
||||
self.connect(self.albumTreeView, QtCore.SIGNAL("addFiles"), QtCore.SIGNAL("addFiles"))
|
||||
self.connect(self.albumTreeView, QtCore.SIGNAL("addDirectory"), QtCore.SIGNAL("addDirectory"))
|
||||
|
||||
self.splitter.addWidget(self.fileTreeView)
|
||||
self.splitter.addWidget(self.albumTreeView)
|
||||
|
||||
self.localMetadataBox = MetadataBox(self, _("Local Metadata"))
|
||||
self.localMetadataBox.setDisabled(True)
|
||||
self.serverMetadataBox = MetadataBox(self, _("Server Metadata"))
|
||||
self.serverMetadataBox.setDisabled(True)
|
||||
|
||||
self.coverArtBox = CoverArtBox(self)
|
||||
if not self.showCoverArtAct.isChecked():
|
||||
self.coverArtBox.hide()
|
||||
|
||||
bottomLayout = QtGui.QHBoxLayout()
|
||||
bottomLayout.addWidget(self.localMetadataBox, 1)
|
||||
bottomLayout.addWidget(self.serverMetadataBox, 1)
|
||||
bottomLayout.addWidget(self.coverArtBox, 0)
|
||||
|
||||
mainLayout = QtGui.QVBoxLayout()
|
||||
mainLayout.addWidget(self.splitter, 1)
|
||||
mainLayout.addLayout(bottomLayout, 0)
|
||||
|
||||
centralWidget.setLayout(mainLayout)
|
||||
|
||||
self.restoreWindowState()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.saveWindowState()
|
||||
event.accept()
|
||||
|
||||
def saveWindowState(self):
|
||||
self.config.persist.set("windowState", self.saveState())
|
||||
isMaximized = int(self.windowState()) & QtCore.Qt.WindowMaximized != 0
|
||||
if isMaximized:
|
||||
# FIXME: this doesn't include the window frame
|
||||
geom = self.normalGeometry()
|
||||
self.config.persist.set("windowPosition", geom.topLeft())
|
||||
self.config.persist.set("windowSize", geom.size())
|
||||
else:
|
||||
self.config.persist.set("windowPosition", self.pos())
|
||||
self.config.persist.set("windowSize", self.size())
|
||||
self.config.persist.set("windowMaximized", isMaximized)
|
||||
self.config.persist.set("viewCoverArt", self.showCoverArtAct.isChecked())
|
||||
self.fileTreeView.saveState()
|
||||
self.albumTreeView.saveState()
|
||||
|
||||
def restoreWindowState(self):
|
||||
self.restoreState(self.config.persist.get("windowState").toByteArray())
|
||||
pos = self.config.persist.get("windowPosition").toPoint()
|
||||
self.move(pos)
|
||||
size = self.config.persist.get("windowSize").toSize()
|
||||
self.resize(size)
|
||||
if self.config.persist.getBool("windowMaximized"):
|
||||
self.setWindowState(QtCore.Qt.WindowMaximized)
|
||||
|
||||
def createStatusBar(self):
|
||||
# TR: The initial status bar message
|
||||
self.statusBar().showMessage(_("Ready"))
|
||||
|
||||
def createActions(self):
|
||||
self.optionsAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarOptions.png"), "&Options...", self)
|
||||
#self.openSettingsAct.setShortcut("Ctrl+O")
|
||||
self.connect(self.optionsAct, QtCore.SIGNAL("triggered()"), self.showOptions)
|
||||
|
||||
self.helpAct = QtGui.QAction(_("&Help..."), self)
|
||||
# TR: Keyboard shortcut for "Help"
|
||||
self.helpAct.setShortcut(QtGui.QKeySequence(_("Ctrl+H")))
|
||||
#self.connect(self.helpAct, QtCore.SIGNAL("triggered()"), self.showHelp)
|
||||
|
||||
self.aboutAct = QtGui.QAction(_("&About..."), self)
|
||||
#self.connect(self.aboutAct, QtCore.SIGNAL("triggered()"), self.showAbout)
|
||||
|
||||
self.addFilesAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarAddFiles.png"), _("&Add Files..."), self)
|
||||
self.addFilesAct.setStatusTip(_("Add files to the tagger"))
|
||||
# TR: Keyboard shortcut for "Add Files..."
|
||||
self.addFilesAct.setShortcut(QtGui.QKeySequence(_("Ctrl+O")))
|
||||
self.connect(self.addFilesAct, QtCore.SIGNAL("triggered()"), self.addFiles)
|
||||
|
||||
self.addDirectoryAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarAddDir.png"), _("A&dd Directory..."), self)
|
||||
self.addDirectoryAct.setStatusTip(_("Add a directory to the tagger"))
|
||||
# TR: Keyboard shortcut for "Add Directory..."
|
||||
self.addDirectoryAct.setShortcut(QtGui.QKeySequence(_("Ctrl+D")))
|
||||
self.connect(self.addDirectoryAct, QtCore.SIGNAL("triggered()"), self.addDirectory)
|
||||
|
||||
self.saveAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarSave.png"), _("&Save Selected Files"), self)
|
||||
# TR: Keyboard shortcut for "Save files"
|
||||
self.saveAct.setShortcut(QtGui.QKeySequence(_("Ctrl+S")))
|
||||
self.saveAct.setEnabled(False)
|
||||
self.connect(self.saveAct, QtCore.SIGNAL("triggered()"), self.save)
|
||||
|
||||
self.submitAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarSubmit.png"), _("S&ubmit PUIDs to MusicBrainz"), self)
|
||||
self.submitAct.setEnabled(False)
|
||||
self.connect(self.submitAct, QtCore.SIGNAL("triggered()"), self.submit)
|
||||
|
||||
self.exitAct = QtGui.QAction(_("E&xit"), self)
|
||||
self.exitAct.setShortcut(QtGui.QKeySequence(_("Ctrl+Q")))
|
||||
self.connect(self.exitAct, QtCore.SIGNAL("triggered()"), self.close)
|
||||
|
||||
self.removeAct = QtGui.QAction(QtGui.QIcon(":/images/remove.png"), _("&Remove"), self)
|
||||
self.removeAct.setShortcut(QtGui.QKeySequence(_("Del")))
|
||||
self.removeAct.setEnabled(False)
|
||||
self.connect(self.removeAct, QtCore.SIGNAL("triggered()"), self.remove)
|
||||
|
||||
self.showFileBrowserAct = QtGui.QAction(_("File &Browser"), self)
|
||||
self.showFileBrowserAct.setCheckable(True)
|
||||
#if self.config.persi.value("persist/viewFileBrowser").toBool():
|
||||
# self.showFileBrowserAct.setChecked(True)
|
||||
# self.connect(self.showFileBrowserAct, QtCore.SIGNAL("triggered()"), self.showFileBrowser)
|
||||
|
||||
self.showCoverArtAct = QtGui.QAction(_("&Cover Art"), self)
|
||||
self.showCoverArtAct.setCheckable(True)
|
||||
if self.config.persist.getBool("viewCoverArt"):
|
||||
self.showCoverArtAct.setChecked(True)
|
||||
self.connect(self.showCoverArtAct, QtCore.SIGNAL("triggered()"), self.showCoverArt)
|
||||
|
||||
self.searchAct = QtGui.QAction(QtGui.QIcon(":/images/search.png"), _("Search"), self)
|
||||
self.connect(self.searchAct, QtCore.SIGNAL("triggered()"), self.search)
|
||||
|
||||
self.listenAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarListen.png"), _("Listen"), self)
|
||||
self.listenAct.setEnabled(False)
|
||||
self.connect(self.listenAct, QtCore.SIGNAL("triggered()"), self.listen)
|
||||
|
||||
self.cdLookupAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarLookup.png"), _("&Lookup CD"), self)
|
||||
self.cdLookupAct.setEnabled(False)
|
||||
self.cdLookupAct.setShortcut(QtGui.QKeySequence(_("Ctrl+L")))
|
||||
|
||||
self.analyzeAct = QtGui.QAction(QtGui.QIcon(":/images/analyze.png"), _("Anal&yze"), self)
|
||||
self.analyzeAct.setEnabled(False)
|
||||
self.analyzeAct.setShortcut(QtGui.QKeySequence(_("Ctrl+Y")))
|
||||
|
||||
self.clusterAct = QtGui.QAction(QtGui.QIcon(":/images/ToolbarCluster.png"), _("Cluster"), self)
|
||||
self.clusterAct.setEnabled(False)
|
||||
self.clusterAct.setShortcut(QtGui.QKeySequence(_("Ctrl+U")))
|
||||
|
||||
def createMenus(self):
|
||||
self.fileMenu = self.menuBar().addMenu(_("&File"))
|
||||
self.fileMenu.addAction(self.addFilesAct)
|
||||
self.fileMenu.addAction(self.addDirectoryAct)
|
||||
self.fileMenu.addSeparator()
|
||||
self.fileMenu.addAction(self.saveAct)
|
||||
self.fileMenu.addAction(self.submitAct)
|
||||
self.fileMenu.addSeparator()
|
||||
self.fileMenu.addAction(self.exitAct)
|
||||
|
||||
self.editMenu = self.menuBar().addMenu(_("&Edit"))
|
||||
self.editMenu.addSeparator()
|
||||
self.editMenu.addAction(self.optionsAct)
|
||||
|
||||
self.viewMenu = self.menuBar().addMenu(_("&View"))
|
||||
self.viewMenu.addAction(self.showFileBrowserAct)
|
||||
self.viewMenu.addAction(self.showCoverArtAct)
|
||||
|
||||
self.menuBar().addSeparator()
|
||||
|
||||
self.helpMenu = self.menuBar().addMenu(_("&Help"))
|
||||
self.helpMenu.addAction(self.helpAct)
|
||||
self.helpMenu.addAction(self.aboutAct)
|
||||
|
||||
def createToolBar(self):
|
||||
self.mainToolBar = self.addToolBar(self.tr("File"))
|
||||
self.mainToolBar.setObjectName("fileToolbar")
|
||||
self.mainToolBar.addAction(self.addFilesAct)
|
||||
self.mainToolBar.addAction(self.addDirectoryAct)
|
||||
self.mainToolBar.addSeparator()
|
||||
self.mainToolBar.addAction(self.saveAct)
|
||||
self.mainToolBar.addAction(self.submitAct)
|
||||
self.mainToolBar.addSeparator()
|
||||
self.mainToolBar.addAction(self.cdLookupAct)
|
||||
self.mainToolBar.addAction(self.analyzeAct)
|
||||
self.mainToolBar.addAction(self.clusterAct)
|
||||
self.mainToolBar.addSeparator()
|
||||
self.mainToolBar.addAction(self.removeAct)
|
||||
self.mainToolBar.addSeparator()
|
||||
self.mainToolBar.addAction(self.optionsAct)
|
||||
self.mainToolBar.addSeparator()
|
||||
self.mainToolBar.addAction(self.listenAct)
|
||||
|
||||
self.searchToolBar = self.addToolBar(_("Search"))
|
||||
self.searchToolBar.setObjectName("searchToolbar")
|
||||
|
||||
searchPanel = QtGui.QWidget(self.searchToolBar)
|
||||
hbox = QtGui.QHBoxLayout(searchPanel)
|
||||
|
||||
self.searchEdit = QtGui.QLineEdit(searchPanel)
|
||||
self.connect(self.searchEdit, QtCore.SIGNAL("returnPressed()"), self.search)
|
||||
hbox.addWidget(self.searchEdit, 0)
|
||||
|
||||
self.searchCombo = QtGui.QComboBox(searchPanel)
|
||||
self.searchCombo.addItem(_("Album"), QtCore.QVariant("album"))
|
||||
self.searchCombo.addItem(_("Artist"), QtCore.QVariant("artist"))
|
||||
self.searchCombo.addItem(_("Track"), QtCore.QVariant("track"))
|
||||
hbox.addWidget(self.searchCombo, 0)
|
||||
|
||||
#button = QtGui.QPushButton(_("&Search"), searchPanel)
|
||||
#self.connect(button, QtCore.SIGNAL("clicked()"), self.search)
|
||||
#hbox.addWidget(button, 0)
|
||||
|
||||
self.searchToolBar.addWidget(searchPanel)
|
||||
self.searchToolBar.addAction(self.searchAct)
|
||||
|
||||
def setStatusBarMessage(self, message):
|
||||
"""Set the status bar message."""
|
||||
self.statusBar().showMessage(message)
|
||||
|
||||
def search(self):
|
||||
"""Search for album, artist or track on MusicBrainz."""
|
||||
text = unicode(self.searchEdit.text())
|
||||
type = unicode(self.searchCombo.itemData(self.searchCombo.currentIndex()).toString())
|
||||
self.log.debug("Search, '%s', %s", text, type)
|
||||
self.emit(QtCore.SIGNAL("search"), text, type)
|
||||
|
||||
def addFiles(self):
|
||||
"""Add files to the tagger."""
|
||||
currentDirectory = self.config.persist.getString("currentDirectory", "")
|
||||
formats = []
|
||||
extensions = []
|
||||
for format in self.tagger.getSupportedFormats():
|
||||
ext = u"*%s" % format[0]
|
||||
formats.append(u"%s (%s)" % (format[1], ext))
|
||||
extensions.append(ext)
|
||||
formats.insert(0, _(u"All Supported Formats") + u" (%s)" % u" ".join(extensions))
|
||||
files = QtGui.QFileDialog.getOpenFileNames(self, "", currentDirectory, u";;".join(formats))
|
||||
if files:
|
||||
files = [unicode(f) for f in files]
|
||||
self.config.persist.set("currentDirectory", os.path.dirname(files[0]))
|
||||
self.emit(QtCore.SIGNAL("addFiles"), files)
|
||||
|
||||
def addDirectory(self):
|
||||
"""Add directory to the tagger."""
|
||||
currentDirectory = self.config.persist.getString("currentDirectory", "")
|
||||
directory = QtGui.QFileDialog.getExistingDirectory(self, "", currentDirectory)
|
||||
if directory:
|
||||
directory = unicode(directory)
|
||||
self.config.persist.set("currentDirectory", directory)
|
||||
self.emit(QtCore.SIGNAL("addDirectory"), directory)
|
||||
|
||||
def showOptions(self):
|
||||
dlg = OptionsDialog(self)
|
||||
dlg.exec_()
|
||||
|
||||
def save(self):
|
||||
files = []
|
||||
for obj in self.selectedObjects:
|
||||
if isinstance(obj, File):
|
||||
files.append(obj)
|
||||
|
||||
if files:
|
||||
self.tagger.saveFiles(files)
|
||||
|
||||
def listen(self):
|
||||
pass
|
||||
|
||||
def submit(self):
|
||||
pass
|
||||
|
||||
def updateFileTreeSelection(self):
|
||||
objs = self.fileTreeView.selectedObjects()
|
||||
|
||||
|
||||
def updateAlbumTreeSelection(self):
|
||||
objs = self.fileTreeView.selectedObjects()
|
||||
|
||||
def updateSelection(self, objects):
|
||||
self.selectedObjects = objects
|
||||
|
||||
canRemove = False
|
||||
canSave = False
|
||||
for obj in objects:
|
||||
if isinstance(obj, File):
|
||||
canRemove = True
|
||||
canSave = True
|
||||
self.removeAct.setEnabled(canRemove)
|
||||
self.saveAct.setEnabled(canSave)
|
||||
|
||||
localMetadata = None
|
||||
serverMetadata = None
|
||||
statusBar = u""
|
||||
if len(objects) == 1:
|
||||
obj = objects[0]
|
||||
if isinstance(obj, File):
|
||||
localMetadata = obj.localMetadata
|
||||
serverMetadata = obj.serverMetadata
|
||||
statusBar = obj.fileName
|
||||
|
||||
if localMetadata:
|
||||
self.localMetadataBox.setArtist(localMetadata["ARTIST"])
|
||||
self.localMetadataBox.setAlbum(localMetadata["ALBUM"])
|
||||
self.localMetadataBox.setTitle(localMetadata["TITLE"])
|
||||
self.localMetadataBox.setDisabled(False)
|
||||
else:
|
||||
self.localMetadataBox.clear()
|
||||
self.localMetadataBox.setDisabled(True)
|
||||
|
||||
if serverMetadata:
|
||||
self.serverMetadataBox.setArtist(serverMetadata["ARTIST"])
|
||||
self.serverMetadataBox.setAlbum(serverMetadata["ALBUM"])
|
||||
self.serverMetadataBox.setTitle(serverMetadata["TITLE"])
|
||||
self.serverMetadataBox.setDisabled(False)
|
||||
else:
|
||||
self.serverMetadataBox.clear()
|
||||
self.serverMetadataBox.setDisabled(True)
|
||||
|
||||
self.setStatusBarMessage(statusBar)
|
||||
|
||||
def remove(self):
|
||||
files = []
|
||||
for obj in self.selectedObjects:
|
||||
if isinstance(obj, File):
|
||||
files.append(obj)
|
||||
|
||||
if files:
|
||||
self.tagger.fileManager.removeFiles(files)
|
||||
|
||||
def showCoverArt(self):
|
||||
"""Show/hide the cover art box."""
|
||||
if self.showCoverArtAct.isChecked():
|
||||
self.coverArtBox.show()
|
||||
else:
|
||||
self.coverArtBox.hide()
|
||||
121
picard/ui/metadatabox.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
class MetadataBox(QtGui.QGroupBox):
|
||||
|
||||
def __init__(self, parent, title):
|
||||
QtGui.QGroupBox.__init__(self, title)
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
self.gridlayout = QtGui.QGridLayout()
|
||||
self.gridlayout.setSpacing(2)
|
||||
|
||||
|
||||
self.titleEdit = QtGui.QLineEdit(self)
|
||||
self.artistEdit = QtGui.QLineEdit(self)
|
||||
self.albumEdit = QtGui.QLineEdit(self)
|
||||
#self.titleEdit = QtGui.QComboBox(self)
|
||||
#self.titleEdit.addItem(u"The Prodigy")
|
||||
#self.titleEdit.addItem(u"Faithless")
|
||||
#self.titleEdit.setEditable(True)
|
||||
#self.titleEdit.setAutoCompletion(True)
|
||||
#self.titleEdit.lineEdit().setText(u"")
|
||||
|
||||
#self.artistEdit = QtGui.QComboBox(self)
|
||||
#self.artistEdit.setEditable(True)
|
||||
|
||||
#self.albumEdit = QtGui.QComboBox(self)
|
||||
#self.albumEdit.setEditable(True)
|
||||
|
||||
self.gridlayout.addWidget(QtGui.QLabel(_("Title:")), 0, 0, QtCore.Qt.AlignRight)
|
||||
self.gridlayout.addWidget(self.titleEdit, 0, 1, 1, 6)
|
||||
self.gridlayout.addWidget(QtGui.QLabel(_("Artist:")), 1, 0, QtCore.Qt.AlignRight)
|
||||
self.gridlayout.addWidget(self.artistEdit, 1, 1, 1, 6)
|
||||
self.gridlayout.addWidget(QtGui.QLabel(_("Album:")), 2, 0, QtCore.Qt.AlignRight)
|
||||
self.gridlayout.addWidget(self.albumEdit, 2, 1, 1, 6)
|
||||
self.gridlayout.addWidget(QtGui.QLabel(_("Track#:")), 3, 0, QtCore.Qt.AlignRight)
|
||||
|
||||
self.trackNumEdit = QtGui.QLineEdit(self)
|
||||
sizePolicy = self.trackNumEdit.sizePolicy()
|
||||
sizePolicy.setHorizontalStretch(2)
|
||||
self.trackNumEdit.setSizePolicy(sizePolicy)
|
||||
|
||||
self.timeEdit = QtGui.QLineEdit(self)
|
||||
sizePolicy = self.timeEdit.sizePolicy()
|
||||
sizePolicy.setHorizontalStretch(2)
|
||||
self.timeEdit.setSizePolicy(sizePolicy)
|
||||
|
||||
#self.dateEdit = QtGui.QDateEdit(self)
|
||||
self.dateEdit = QtGui.QLineEdit(self)
|
||||
self.dateEdit.setInputMask("0000-00-00")
|
||||
sizePolicy = self.dateEdit.sizePolicy()
|
||||
sizePolicy.setHorizontalStretch(4)
|
||||
self.dateEdit.setSizePolicy(sizePolicy)
|
||||
|
||||
self.gridlayout.addWidget(self.trackNumEdit, 3, 1)
|
||||
self.gridlayout.addWidget(QtGui.QLabel(_("Time:")), 3, 2, QtCore.Qt.AlignRight)
|
||||
self.gridlayout.addWidget(self.timeEdit, 3, 3)
|
||||
self.gridlayout.addWidget(QtGui.QLabel(_("Date:")), 3, 4, QtCore.Qt.AlignRight)
|
||||
self.gridlayout.addWidget(self.dateEdit, 3, 5)
|
||||
|
||||
self.lookupButton = QtGui.QPushButton(_("Lookup"), self)
|
||||
self.connect(self.lookupButton, QtCore.SIGNAL("clicked()"), self.lookup)
|
||||
|
||||
self.gridlayout.addWidget(self.lookupButton, 3, 6)
|
||||
|
||||
#hbox = QtGui.QHBoxLayout()
|
||||
#hbox.addWidget(QtGui.QLineEdit(self), 1)
|
||||
#hbox.addWidget(QtGui.QLabel(_("Genre:")), 0)
|
||||
#hbox.addWidget(QtGui.QLineEdit(self), 2)
|
||||
#self.gridlayout.addLayout(hbox, 4, 1, 1, 5)
|
||||
|
||||
self.vbox = QtGui.QVBoxLayout(self)
|
||||
self.vbox.addLayout(self.gridlayout, 0)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
def setDisabled(self, val):
|
||||
self.titleEdit.setDisabled(val)
|
||||
self.artistEdit.setDisabled(val)
|
||||
self.albumEdit.setDisabled(val)
|
||||
self.trackNumEdit.setDisabled(val)
|
||||
self.timeEdit.setDisabled(val)
|
||||
self.dateEdit.setDisabled(val)
|
||||
self.lookupButton.setDisabled(val)
|
||||
|
||||
def setTitle(self, text):
|
||||
self.titleEdit.setText(text)
|
||||
|
||||
def setArtist(self, text):
|
||||
self.artistEdit.setText(text)
|
||||
|
||||
def setAlbum(self, text):
|
||||
self.albumEdit.setText(text)
|
||||
|
||||
def clear(self):
|
||||
self.setTitle(u"")
|
||||
self.setArtist(u"")
|
||||
self.setAlbum(u"")
|
||||
|
||||
def lookup(self):
|
||||
self.log.debug("lookup")
|
||||
|
||||
59
picard/ui/optionsdialog.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
class OptionsDialog(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self):
|
||||
self.setWindowTitle(_("Options"))
|
||||
|
||||
self.splitter = QtGui.QSplitter(self)
|
||||
|
||||
self.treeWidget = QtGui.QTreeWidget(self.splitter)
|
||||
self.splitter.addWidget(self.treeWidget)
|
||||
self.splitter.addWidget(QtGui.QWidget())
|
||||
|
||||
self.okButton = QtGui.QPushButton(_("OK"), self)
|
||||
self.connect(self.okButton, QtCore.SIGNAL("clicked()"), self.onOk)
|
||||
self.cancelButton = QtGui.QPushButton(_("Cancel"), self)
|
||||
self.connect(self.cancelButton, QtCore.SIGNAL("clicked()"), self.onCancel)
|
||||
|
||||
buttonLayout = QtGui.QHBoxLayout()
|
||||
buttonLayout.addStretch()
|
||||
buttonLayout.addWidget(self.okButton, 0)
|
||||
buttonLayout.addWidget(self.cancelButton, 0)
|
||||
|
||||
mainLayout = QtGui.QVBoxLayout()
|
||||
mainLayout.addWidget(self.splitter)
|
||||
mainLayout.addLayout(buttonLayout)
|
||||
|
||||
self.setLayout(mainLayout)
|
||||
|
||||
def onOk(self):
|
||||
print "ok"
|
||||
self.close()
|
||||
|
||||
def onCancel(self):
|
||||
print "cancel"
|
||||
self.close()
|
||||
222
picard/ui/tageditor.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
class TagEditor(QtGui.QDialog):
|
||||
|
||||
def __init__(self, metadata, parent=None):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.metadata = metadata
|
||||
self.setupUi()
|
||||
self.load()
|
||||
|
||||
def setupUi(self):
|
||||
self.setWindowTitle(_("Tag Editor"))
|
||||
|
||||
self.tabs = QtGui.QTabWidget(self)
|
||||
|
||||
self.gridlayout1 = QtGui.QGridLayout()
|
||||
self.gridlayout1.setSpacing(2)
|
||||
|
||||
row = 0
|
||||
|
||||
self.gridlayout1.addWidget(QtGui.QLabel(_("Title:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
self.titleEdit = QtGui.QLineEdit(self.tabs)
|
||||
self.gridlayout1.addWidget(self.titleEdit, row, 1)
|
||||
|
||||
row += 1
|
||||
|
||||
self.gridlayout1.addWidget(QtGui.QLabel(_("Album:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
self.albumEdit = QtGui.QLineEdit(self.tabs)
|
||||
self.gridlayout1.addWidget(self.albumEdit, row, 1)
|
||||
|
||||
row += 1
|
||||
|
||||
self.gridlayout1.addWidget(QtGui.QLabel(_("Artist:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
self.artistEdit = QtGui.QLineEdit(self.tabs)
|
||||
self.gridlayout1.addWidget(self.artistEdit, row, 1)
|
||||
|
||||
row += 1
|
||||
|
||||
self.gridlayout1.addWidget(QtGui.QLabel(_("Track:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
|
||||
self.trackNumberEdit = QtGui.QLineEdit(self.tabs)
|
||||
self.totalTracksEdit = QtGui.QLineEdit(self.tabs)
|
||||
|
||||
hbox = QtGui.QHBoxLayout()
|
||||
hbox.addWidget(self.trackNumberEdit, 1)
|
||||
hbox.addWidget(QtGui.QLabel(_(u" of ")))
|
||||
hbox.addWidget(self.totalTracksEdit, 1)
|
||||
hbox.addStretch(9)
|
||||
|
||||
self.gridlayout1.addLayout(hbox, row, 1)
|
||||
|
||||
row += 1
|
||||
|
||||
self.gridlayout1.addWidget(QtGui.QLabel(_("Disc:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
|
||||
self.discNumberEdit = QtGui.QLineEdit(self.tabs)
|
||||
self.totalDiscsEdit = QtGui.QLineEdit(self.tabs)
|
||||
|
||||
hbox = QtGui.QHBoxLayout()
|
||||
hbox.addWidget(self.discNumberEdit, 1)
|
||||
hbox.addWidget(QtGui.QLabel(_(u" of ")))
|
||||
hbox.addWidget(self.totalDiscsEdit, 1)
|
||||
hbox.addStretch(9)
|
||||
|
||||
self.gridlayout1.addLayout(hbox, row, 1)
|
||||
|
||||
row += 1
|
||||
|
||||
self.gridlayout1.addWidget(QtGui.QLabel(_("Release date:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
|
||||
self.releaseDateEdit = QtGui.QLineEdit(self.tabs)
|
||||
self.releaseDateEdit.setInputMask("0000-00-00")
|
||||
|
||||
hbox = QtGui.QHBoxLayout()
|
||||
hbox.addWidget(self.releaseDateEdit, 1)
|
||||
hbox.addStretch(4)
|
||||
|
||||
self.gridlayout1.addLayout(hbox, row, 1)
|
||||
|
||||
row += 1
|
||||
|
||||
#self.gridlayout1.addWidget(QtGui.QLabel(_(u"Release date:")), row, 0, QtCore.Qt.AlignLeft)
|
||||
#self.gridlayout1.addWidget(self.discNumberEdit, row, 1)
|
||||
|
||||
vbox = QtGui.QVBoxLayout()
|
||||
vbox.addLayout(self.gridlayout1)
|
||||
vbox.addStretch()
|
||||
|
||||
self.basicTags = QtGui.QWidget()
|
||||
self.basicTags.setLayout(vbox)
|
||||
|
||||
self.tabs.addTab(self.basicTags, _("&Basic"))
|
||||
|
||||
self.gridlayout2 = QtGui.QGridLayout()
|
||||
self.gridlayout2.setSpacing(2)
|
||||
|
||||
details = [
|
||||
["sortnameEdit", _("Artist sortname:")],
|
||||
["albumArtistEdit", _("Album artist:")],
|
||||
["albumArtistSortnameEdit", _("Album artist sortname:")],
|
||||
["composerEdit", _("Composer:")],
|
||||
["conductorEdit", _("Conductor:")],
|
||||
["ensembleEdit", _("Ensemble:")],
|
||||
["lyricistEdit", _("Lyricist:")],
|
||||
["arrangerEdit", _("Arranger:")],
|
||||
["producerEdit", _("Producer:")],
|
||||
["engineerEdit", _("Engineer:")],
|
||||
["remixerEdit", _("Remixer:")],
|
||||
["mixDjEdit", _("Mix DJ:")],
|
||||
]
|
||||
i = 0
|
||||
for item in details:
|
||||
self.gridlayout2.addWidget(QtGui.QLabel(item[1]), i, 0, QtCore.Qt.AlignLeft)
|
||||
edit = QtGui.QLineEdit(self.tabs)
|
||||
self.gridlayout2.addWidget(edit, i, 1)
|
||||
setattr(self, item[0], edit)
|
||||
i += 1
|
||||
|
||||
vbox = QtGui.QVBoxLayout()
|
||||
vbox.addLayout(self.gridlayout2)
|
||||
vbox.addStretch()
|
||||
|
||||
self.detailsTags = QtGui.QWidget()
|
||||
self.detailsTags.setLayout(vbox)
|
||||
|
||||
self.tabs.addTab(self.detailsTags, _("&Details"))
|
||||
self.tabs.addTab(QtGui.QWidget(), _("&MusicBrainz"))
|
||||
self.tabs.addTab(QtGui.QWidget(), _("&Album Art"))
|
||||
self.tabs.addTab(QtGui.QWidget(), _("&Info"))
|
||||
|
||||
self.okButton = QtGui.QPushButton(_("OK"), self)
|
||||
self.connect(self.okButton, QtCore.SIGNAL("clicked()"), self.onOk)
|
||||
self.cancelButton = QtGui.QPushButton(_("Cancel"), self)
|
||||
self.connect(self.cancelButton, QtCore.SIGNAL("clicked()"), self.onCancel)
|
||||
|
||||
buttonLayout = QtGui.QHBoxLayout()
|
||||
buttonLayout.addStretch()
|
||||
buttonLayout.addWidget(self.okButton, 0)
|
||||
buttonLayout.addWidget(self.cancelButton, 0)
|
||||
|
||||
mainLayout = QtGui.QVBoxLayout()
|
||||
mainLayout.addWidget(self.tabs)
|
||||
mainLayout.addLayout(buttonLayout)
|
||||
|
||||
self.setLayout(mainLayout)
|
||||
self.resize(QtCore.QSize(500, 350))
|
||||
|
||||
def onOk(self):
|
||||
self.save()
|
||||
self.close()
|
||||
|
||||
def onCancel(self):
|
||||
self.close()
|
||||
|
||||
def loadField(self, name, edit):
|
||||
text = self.metadata.get(name, u"")
|
||||
edit.setText(text)
|
||||
|
||||
def load(self):
|
||||
self.loadField(u"TITLE", self.titleEdit)
|
||||
self.loadField(u"ALBUM", self.albumEdit)
|
||||
self.loadField(u"ARTIST", self.artistEdit)
|
||||
self.loadField(u"ALBUMARTIST", self.albumArtistEdit)
|
||||
self.loadField(u"TRACKNUMBER", self.trackNumberEdit)
|
||||
self.loadField(u"TOTALTRACKS", self.totalTracksEdit)
|
||||
self.loadField(u"DISCNUMBER", self.discNumberEdit)
|
||||
self.loadField(u"TOTALDISCS", self.totalDiscsEdit)
|
||||
# TODO: DATE
|
||||
self.loadField(u"COMPOSER", self.composerEdit)
|
||||
self.loadField(u"CONDUCTOR", self.conductorEdit)
|
||||
self.loadField(u"ENSEMBLE", self.ensembleEdit)
|
||||
self.loadField(u"LYRICIST", self.lyricistEdit)
|
||||
self.loadField(u"ARRANGER", self.arrangerEdit)
|
||||
self.loadField(u"PRODUCER", self.producerEdit)
|
||||
self.loadField(u"ENGINEER", self.engineerEdit)
|
||||
self.loadField(u"REMIXER", self.remixerEdit)
|
||||
self.loadField(u"MIXDJ", self.mixDjEdit)
|
||||
|
||||
def saveField(self, name, edit):
|
||||
text = unicode(edit.text())
|
||||
if text or name in self.metadata:
|
||||
self.metadata.set(name, text)
|
||||
|
||||
def save(self):
|
||||
self.saveField(u"TITLE", self.titleEdit)
|
||||
self.saveField(u"ALBUM", self.albumEdit)
|
||||
self.saveField(u"ARTIST", self.artistEdit)
|
||||
self.saveField(u"ALBUMARTIST", self.albumArtistEdit)
|
||||
self.saveField(u"TRACKNUMBER", self.trackNumberEdit)
|
||||
self.saveField(u"TOTALTRACKS", self.totalTracksEdit)
|
||||
self.saveField(u"DISCNUMBER", self.discNumberEdit)
|
||||
self.saveField(u"TOTALDISCS", self.totalDiscsEdit)
|
||||
# TODO: DATE
|
||||
self.saveField(u"COMPOSER", self.composerEdit)
|
||||
self.saveField(u"CONDUCTOR", self.conductorEdit)
|
||||
self.saveField(u"ENSEMBLE", self.ensembleEdit)
|
||||
self.saveField(u"LYRICIST", self.lyricistEdit)
|
||||
self.saveField(u"ARRANGER", self.arrangerEdit)
|
||||
self.saveField(u"PRODUCER", self.producerEdit)
|
||||
self.saveField(u"ENGINEER", self.engineerEdit)
|
||||
self.saveField(u"REMIXER", self.remixerEdit)
|
||||
self.saveField(u"MIXDJ", self.mixDjEdit)
|
||||
|
||||
49
picard/util.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2004 Robert Kaye
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
_ioEncoding = sys.getfilesystemencoding()
|
||||
|
||||
def setIoEncoding(encoding):
|
||||
_ioEncoding = encoding
|
||||
|
||||
def encodeFileName(fileName):
|
||||
if isinstance(fileName, unicode):
|
||||
if os.path.supports_unicode_filenames:
|
||||
return fileName
|
||||
else:
|
||||
return fileName.encode(_ioEncoding, 'replace')
|
||||
else:
|
||||
return fileName
|
||||
|
||||
def decodeFileName(fileName):
|
||||
if isinstance(fileName, unicode):
|
||||
return fileName
|
||||
else:
|
||||
return fileName.decode(_ioEncoding)
|
||||
|
||||
def formatTime(ms):
|
||||
if ms == 0:
|
||||
return u"?:??"
|
||||
else:
|
||||
return u"%d:%02d" % (ms / 60000, (ms / 1000) % 60)
|
||||
|
||||
121
picard/worker.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from Queue import Queue
|
||||
import os.path
|
||||
import sys
|
||||
from picard import util
|
||||
|
||||
class WorkerThread(QtCore.QThread):
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.exitThread = False
|
||||
self.queue = Queue()
|
||||
self.files = []
|
||||
|
||||
def start(self):
|
||||
self.log.debug("Starting the worker thread")
|
||||
QtCore.QThread.start(self)
|
||||
|
||||
def stop(self):
|
||||
self.log.debug("Stopping the worker thread")
|
||||
if self.isRunning():
|
||||
self.exitThread = True
|
||||
self.queue.put(None)
|
||||
self.wait()
|
||||
|
||||
def run(self):
|
||||
while not self.exitThread:
|
||||
item = self.queue.get(True)
|
||||
if not item:
|
||||
continue
|
||||
|
||||
item[0](item)
|
||||
|
||||
if self.queue.empty():
|
||||
# TR: Status bar message
|
||||
message = QtCore.QString(_(u"Done"))
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
def loadAlbum(self, album):
|
||||
"""Load the album information from MusicBrainz."""
|
||||
self.queue.put((self.doLoadAlbum, album))
|
||||
|
||||
def doLoadAlbum(self, args):
|
||||
album = args[1]
|
||||
|
||||
message = QtCore.QString(_(u"Loading album %s ...") % album.id)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
album.load()
|
||||
self.emit(QtCore.SIGNAL("albumLoaded(const QString &)"), album.id)
|
||||
|
||||
def readDirectory(self, directory):
|
||||
"""Read the directory recursively and add all files to the tagger."""
|
||||
self.queue.put((self.doReadDirectory, directory))
|
||||
|
||||
def doReadDirectory(self, args):
|
||||
root = args[1]
|
||||
# Show status bar message
|
||||
message = QtCore.QString(_(u"Reading directory %s ...") % root)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
# Read the directory listing
|
||||
files = QtCore.QStringList()
|
||||
for name in os.listdir(util.encodeFileName(root)):
|
||||
name = os.path.join(root, name)
|
||||
if os.path.isdir(name):
|
||||
self.readDirectory(name)
|
||||
else:
|
||||
files.append(QtCore.QString(util.decodeFileName(name)))
|
||||
if files:
|
||||
self.emit(QtCore.SIGNAL("addFiles(const QStringList &)"), files)
|
||||
|
||||
def readFile(self, fileName, opener):
|
||||
self.queue.put((self.doReadFile, (fileName, opener)))
|
||||
|
||||
def doReadFile(self, args):
|
||||
fileName, opener = args[1]
|
||||
|
||||
# Show status bar message
|
||||
message = QtCore.QString(_(u"Reading file %s ...") % fileName)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
# Load files
|
||||
files = opener(fileName)
|
||||
|
||||
# And add them to the file manager
|
||||
for file in files:
|
||||
self.tagger.fileManager.addFile(file)
|
||||
|
||||
def saveFile(self, file):
|
||||
self.queue.put((self.doSaveFile, file))
|
||||
|
||||
def doSaveFile(self, args):
|
||||
file = args[1]
|
||||
fileName = file.fileName
|
||||
|
||||
self.log.debug("Saving file %r", file)
|
||||
|
||||
# Show status bar message
|
||||
message = QtCore.QString(_(u"Saving file %s ...") % fileName)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
|
||||
85
picard/worker_.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from PyQt4 import QtCore
|
||||
from Queue import Queue
|
||||
from threading import Thread
|
||||
import os.path
|
||||
import sys
|
||||
from picard import util
|
||||
|
||||
class WorkerThread(Thread, QtCore.QObject):
|
||||
|
||||
def __init__(self):
|
||||
QtCore.QObject.__init__(self)
|
||||
Thread.__init__(self)
|
||||
self.exitThread = False
|
||||
self.queue = Queue()
|
||||
self.files = []
|
||||
|
||||
def stop(self):
|
||||
"""Stop the thread"""
|
||||
self.exitThread = True
|
||||
self.queue.put(None)
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
while not self.exitThread:
|
||||
item = self.queue.get(True)
|
||||
if not item:
|
||||
continue
|
||||
|
||||
item[0](item)
|
||||
|
||||
if self.queue.empty():
|
||||
# TR: Status bar message
|
||||
message = QtCore.QString(_(u"Done"))
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
def loadAlbum(self, album):
|
||||
"""Load the album information from MusicBrainz."""
|
||||
self.queue.put((self.doLoadAlbum, album))
|
||||
|
||||
def doLoadAlbum(self, args):
|
||||
album = args[1]
|
||||
|
||||
message = QtCore.QString(_(u"Loading album %s ...") % album.id)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
album.load()
|
||||
self.emit(QtCore.SIGNAL("albumLoaded(const QString &)"), album.id)
|
||||
|
||||
def readDirectory(self, directory):
|
||||
"""Read the directory recursively and add all files to the tagger."""
|
||||
self.queue.put((self.doReadDirectory, directory))
|
||||
|
||||
def doReadDirectory(self, args):
|
||||
root = args[1]
|
||||
# Show status bar message
|
||||
message = QtCore.QString(_(u"Reading directory %s ...") % root)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
# Read the directory listing
|
||||
files = QtCore.QStringList()
|
||||
for name in os.listdir(util.encodeFileName(root)):
|
||||
name = os.path.join(root, name)
|
||||
if os.path.isdir(name):
|
||||
self.readDirectory(name)
|
||||
else:
|
||||
files.append(QtCore.QString(util.decodeFileName(name)))
|
||||
if files:
|
||||
self.emit(QtCore.SIGNAL("addFiles(const QStringList &)"), files)
|
||||
|
||||
def readFile(self, fileName, opener):
|
||||
self.queue.put((self.doReadFile, (fileName, opener)))
|
||||
|
||||
def doReadFile(self, args):
|
||||
fileName, opener = args[1]
|
||||
|
||||
# Show status bar message
|
||||
message = QtCore.QString(_(u"Reading file %s ...") % fileName)
|
||||
self.emit(QtCore.SIGNAL("statusBarMessage(const QString &)"), message)
|
||||
|
||||
# Load files
|
||||
files = opener(fileName)
|
||||
|
||||
# And add them to the file manager
|
||||
for file in files:
|
||||
self.tagger.fileManager.addFile(file)
|
||||
|
||||