Add original files from 25.02.2009 19:20
This commit is contained in:
342
License.txt
Normal file
342
License.txt
Normal file
@@ -0,0 +1,342 @@
|
||||
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) <year> <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) year 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.
|
||||
|
||||
|
||||
278
hash58.py
Normal file
278
hash58.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# hash generation library for rePear, the iPod database management tool
|
||||
# Copyright (C) 2008 Martin J. Fiedler <martin.fiedler@gmx.net>
|
||||
# based on original code by William Whistler (wtbw)
|
||||
#
|
||||
# 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
|
||||
|
||||
import os, types, re, sha
|
||||
|
||||
try:
|
||||
import _winreg
|
||||
HaveWin32 = True
|
||||
except ImportError:
|
||||
HaveWin32 = False
|
||||
|
||||
def GetFWIDs_Win32():
|
||||
# phase 1: enumerate all mass storage devices
|
||||
if not HaveWin32: return []
|
||||
try:
|
||||
key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum", 0, _winreg.KEY_QUERY_VALUE)
|
||||
except:
|
||||
raise
|
||||
return []
|
||||
devs = []
|
||||
devid = 0
|
||||
try:
|
||||
while True:
|
||||
dev = _winreg.QueryValueEx(key, str(devid))[0]
|
||||
if type(dev) == types.UnicodeType:
|
||||
dev = dev.encode('ascii', 'replace')
|
||||
devs.append(dev.upper())
|
||||
devid += 1
|
||||
except:
|
||||
pass
|
||||
key.Close()
|
||||
|
||||
# phase 2: find iPods and their FWIDs there (I'm being very careful here)
|
||||
fwids = []
|
||||
for dev in devs:
|
||||
dev = dev.upper().replace("\\", "&").split("&")
|
||||
info = dict([x.split('_', 1) for x in dev if '_' in x])
|
||||
vendor = info.get('VEN', None) or info.get('VID', None)
|
||||
product = info.get('DEV', None) or info.get('PROD', None) or info.get('PID', None)
|
||||
if not(vendor in ('APPLE', '05AC')): continue
|
||||
if (product != 'IPOD') and not(product[:2] in ('12', '13')): continue
|
||||
fwid = dev[-2].upper()
|
||||
for item in dev:
|
||||
if item.startswith("000A27"): fwid = item
|
||||
if len(fwid) == 16: fwids.append(fwid)
|
||||
return fwids
|
||||
|
||||
def GetFWIDs_Linux26():
|
||||
LINUX_DEVDIR_BASE = "/sys/bus/usb/devices"
|
||||
try:
|
||||
devs = os.listdir(LINUX_DEVDIR_BASE)
|
||||
except OSError:
|
||||
return []
|
||||
def sysfile(filename):
|
||||
try:
|
||||
f = open(os.path.join(devdir, filename), "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
return data.split("\0", 1)[0].strip().upper()
|
||||
except IOError:
|
||||
return ""
|
||||
fwids = []
|
||||
re_devdir = re.compile(r'^\d+-\d+$')
|
||||
for dev in devs:
|
||||
if not re_devdir.match(dev): continue
|
||||
devdir = os.path.join(LINUX_DEVDIR_BASE, dev)
|
||||
if not os.path.isdir(devdir): continue
|
||||
if not((sysfile("idVendor") == "05AC") \
|
||||
or (sysfile("manufacturer") == "APPLE")): continue
|
||||
if not((sysfile("idProduct")[:2] in ("12", "13")) \
|
||||
or (sysfile("product") == "IPOD")): continue
|
||||
fwid = sysfile("serial")
|
||||
if fwid.startswith("000A27"):
|
||||
fwids.append(fwid)
|
||||
return fwids
|
||||
|
||||
re_ioreg = re.compile(r'"(.*?)"\s+=\s+"?(.*?)"?$')
|
||||
def GetFWIDs_Darwin():
|
||||
devs = []
|
||||
try:
|
||||
f = os.popen("/usr/sbin/ioreg -l", 'r')
|
||||
valid = False
|
||||
for line in f:
|
||||
line = line.strip("\r\n\t |+-")
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith('o'):
|
||||
valid = (line[1:].strip().split()[0].split('@', 1)[0] == "iPod")
|
||||
elif valid and line.startswith('"'):
|
||||
m = re_ioreg.match(line)
|
||||
if m:
|
||||
key, value = m.groups()
|
||||
if (key.lower() == "usb serial number") and (len(value) == 16):
|
||||
devs.append(value.upper())
|
||||
f.close()
|
||||
except (OSError, IOError, EOFError):
|
||||
pass
|
||||
return devs
|
||||
|
||||
def GetFWIDs():
|
||||
if os.name == 'nt':
|
||||
return GetFWIDs_Win32()
|
||||
elif os.name == 'posix':
|
||||
try:
|
||||
uname = os.uname()[0].lower()
|
||||
except (AttributeError, OSError):
|
||||
return []
|
||||
if uname == 'linux':
|
||||
return GetFWIDs_Linux26()
|
||||
elif uname == 'darwin':
|
||||
return GetFWIDs_Darwin()
|
||||
return []
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
|
||||
inv = [
|
||||
0x74, 0x85, 0x96, 0xA7, 0xB8, 0xC9, 0xDA, 0xEB, 0xFC, 0x0D, 0x1E, 0x2F, 0x40, 0x51, 0x62, 0x73,
|
||||
0x84, 0x95, 0xA6, 0xB7, 0xC8, 0xD9, 0xEA, 0xFB, 0x0C, 0x1D, 0x2E, 0x3F, 0x50, 0x61, 0x72, 0x83,
|
||||
0x94, 0xA5, 0xB6, 0xC7, 0xD8, 0xE9, 0xFA, 0x0B, 0x1C, 0x2D, 0x3E, 0x4F, 0x60, 0x71, 0x82, 0x93,
|
||||
0xA4, 0xB5, 0xC6, 0xD7, 0xE8, 0xF9, 0x0A, 0x1B, 0x2C, 0x3D, 0x4E, 0x5F, 0x70, 0x81, 0x92, 0xA3,
|
||||
0xB4, 0xC5, 0xD6, 0xE7, 0xF8, 0x09, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E, 0x6F, 0x80, 0x91, 0xA2, 0xB3,
|
||||
0xC4, 0xD5, 0xE6, 0xF7, 0x08, 0x19, 0x2A, 0x3B, 0x4C, 0x5D, 0x6E, 0x7F, 0x90, 0xA1, 0xB2, 0xC3,
|
||||
0xD4, 0xE5, 0xF6, 0x07, 0x18, 0x29, 0x3A, 0x4B, 0x5C, 0x6D, 0x7E, 0x8F, 0xA0, 0xB1, 0xC2, 0xD3,
|
||||
0xE4, 0xF5, 0x06, 0x17, 0x28, 0x39, 0x4A, 0x5B, 0x6C, 0x7D, 0x8E, 0x9F, 0xB0, 0xC1, 0xD2, 0xE3,
|
||||
0xF4, 0x05, 0x16, 0x27, 0x38, 0x49, 0x5A, 0x6B, 0x7C, 0x8D, 0x9E, 0xAF, 0xC0, 0xD1, 0xE2, 0xF3,
|
||||
0x04, 0x15, 0x26, 0x37, 0x48, 0x59, 0x6A, 0x7B, 0x8C, 0x9D, 0xAE, 0xBF, 0xD0, 0xE1, 0xF2, 0x03,
|
||||
0x14, 0x25, 0x36, 0x47, 0x58, 0x69, 0x7A, 0x8B, 0x9C, 0xAD, 0xBE, 0xCF, 0xE0, 0xF1, 0x02, 0x13,
|
||||
0x24, 0x35, 0x46, 0x57, 0x68, 0x79, 0x8A, 0x9B, 0xAC, 0xBD, 0xCE, 0xDF, 0xF0, 0x01, 0x12, 0x23,
|
||||
0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x9A, 0xAB, 0xBC, 0xCD, 0xDE, 0xEF, 0x00, 0x11, 0x22, 0x33,
|
||||
0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x10, 0x21, 0x32, 0x43,
|
||||
0x54, 0x65, 0x76, 0x87, 0x98, 0xA9, 0xBA, 0xCB, 0xDC, 0xED, 0xFE, 0x0F, 0x20, 0x31, 0x42, 0x53,
|
||||
0x64, 0x75, 0x86, 0x97, 0xA8, 0xB9, 0xCA, 0xDB, 0xEC, 0xFD, 0x0E, 0x1F, 0x30, 0x41, 0x52, 0x63
|
||||
]
|
||||
|
||||
table1 = [
|
||||
0x3A, 0x3F, 0x3E, 0x72, 0xBD, 0xA2, 0xD6, 0xB4, 0x63, 0xC0, 0x6E, 0x62, 0x59, 0x1E, 0xE2, 0x71,
|
||||
0xB5, 0x0D, 0xE8, 0x0C, 0x25, 0x38, 0xCE, 0x23, 0x7C, 0xB7, 0xAD, 0x16, 0xDF, 0x47, 0x3D, 0xB3,
|
||||
0x7E, 0x8C, 0xAA, 0x61, 0x31, 0x66, 0xBE, 0x4F, 0x97, 0x14, 0x54, 0xF0, 0x70, 0xEB, 0x30, 0xC4,
|
||||
0x27, 0x4E, 0xFA, 0x1A, 0x2B, 0x11, 0xF4, 0x45, 0x8E, 0x5D, 0x73, 0xED, 0x22, 0x2E, 0x7D, 0xA4,
|
||||
0x28, 0xDA, 0x2F, 0xC5, 0x92, 0x09, 0x05, 0x13, 0x9D, 0x32, 0x51, 0x4A, 0xC8, 0xBA, 0x96, 0xA7,
|
||||
0x6A, 0x50, 0xF3, 0xBC, 0x93, 0xBF, 0xB0, 0xD2, 0xD5, 0x82, 0x19, 0x98, 0x35, 0xCF, 0x6B, 0xB6,
|
||||
0x83, 0x56, 0x15, 0xF2, 0x9A, 0x9C, 0xCA, 0x74, 0x34, 0x58, 0x8D, 0xA6, 0x03, 0xFF, 0x46, 0x7B,
|
||||
0xD0, 0x7A, 0x33, 0x76, 0xDD, 0xAC, 0xCB, 0x24, 0x7F, 0xB1, 0x85, 0x60, 0xC3, 0x26, 0x8A, 0x1D,
|
||||
0x1C, 0x8F, 0x2A, 0xEF, 0x06, 0xDE, 0x67, 0x5E, 0xE7, 0xAE, 0xD9, 0xCC, 0x07, 0x6C, 0xF8, 0x0A,
|
||||
0xD3, 0x40, 0x36, 0x1F, 0x2D, 0x95, 0x43, 0xDB, 0x01, 0x89, 0x4B, 0xF7, 0xB9, 0x39, 0xC2, 0x52,
|
||||
0x53, 0xFD, 0x65, 0xF5, 0x68, 0xC1, 0xC7, 0x9F, 0x4D, 0xEA, 0xAF, 0x6D, 0x10, 0x44, 0x87, 0xD8,
|
||||
0xEE, 0x1B, 0xFE, 0x3C, 0xDC, 0x84, 0x69, 0x48, 0x6F, 0xD1, 0x57, 0x55, 0xD4, 0xA5, 0x49, 0x5B,
|
||||
0xE5, 0x0B, 0x94, 0xC9, 0x5F, 0xE1, 0x17, 0x81, 0xBB, 0xEC, 0xD7, 0xC6, 0x02, 0x4C, 0x42, 0x75,
|
||||
0xA3, 0x99, 0xE4, 0xA1, 0x9B, 0x5A, 0xF1, 0x29, 0xA0, 0x64, 0x9E, 0x18, 0x41, 0x80, 0x2C, 0x79,
|
||||
0x20, 0x8B, 0xAB, 0x90, 0x08, 0xB8, 0xA9, 0x77, 0x12, 0xF9, 0x0E, 0x88, 0xE9, 0x04, 0xFB, 0x86,
|
||||
0x0F, 0xE0, 0xA8, 0x5C, 0xE6, 0x21, 0xCD, 0x3B, 0x00, 0x78, 0xFC, 0xF6, 0xE3, 0x37, 0xB2, 0x91
|
||||
]
|
||||
|
||||
table2 = [
|
||||
0xF3, 0xE4, 0x1B, 0x38, 0xE5, 0x6F, 0xE8, 0x9D, 0x3E, 0x55, 0xBA, 0xC7, 0xAC, 0xEA, 0x66, 0xA2,
|
||||
0xB9, 0x7A, 0x34, 0x43, 0x02, 0x4E, 0xFE, 0x36, 0x41, 0x57, 0x1A, 0xB1, 0x31, 0x87, 0x04, 0x52,
|
||||
0x21, 0x22, 0xE1, 0x13, 0x7F, 0x03, 0x3A, 0x90, 0xF7, 0x69, 0x78, 0x12, 0x83, 0x0B, 0x9A, 0x97,
|
||||
0x4D, 0xB7, 0x8C, 0xBF, 0x2D, 0x94, 0xD1, 0x93, 0x2F, 0x42, 0x23, 0xA4, 0xE0, 0x92, 0xDC, 0x68,
|
||||
0xD3, 0xDD, 0xAF, 0x91, 0x9F, 0xED, 0x3D, 0x8F, 0xA1, 0x51, 0xD9, 0xE9, 0x70, 0x28, 0xEF, 0xB3,
|
||||
0x49, 0xA5, 0x0D, 0xC5, 0xD0, 0x60, 0xB4, 0x2B, 0x07, 0xF8, 0xDF, 0xE6, 0x16, 0xC0, 0x30, 0x71,
|
||||
0x85, 0xFD, 0x72, 0x95, 0x29, 0x79, 0x0A, 0x7B, 0x46, 0x11, 0x7D, 0x88, 0x1D, 0x2A, 0x48, 0x1F,
|
||||
0x45, 0x89, 0x47, 0xEE, 0xBB, 0xBE, 0x6E, 0xC3, 0x6C, 0xCE, 0x10, 0x5A, 0x2C, 0xCA, 0xFB, 0xB2,
|
||||
0xCB, 0x1C, 0x9C, 0xEC, 0x2E, 0x56, 0x59, 0x9B, 0xA6, 0x53, 0xAE, 0x17, 0x25, 0xC1, 0x3F, 0x6A,
|
||||
0x0F, 0x09, 0x01, 0xA3, 0xD6, 0xA0, 0xD8, 0x08, 0xE3, 0x74, 0x06, 0x6D, 0x19, 0x98, 0x1E, 0x77,
|
||||
0x76, 0xBC, 0xEB, 0x3C, 0xB0, 0xC4, 0xC8, 0x64, 0x0E, 0x86, 0x63, 0xD7, 0xDB, 0xBD, 0xA7, 0x82,
|
||||
0x39, 0x4F, 0x27, 0xD2, 0x5F, 0x73, 0xF4, 0x75, 0x6B, 0xC2, 0xD5, 0x67, 0x5D, 0x80, 0xAB, 0x81,
|
||||
0xDE, 0xF0, 0xAD, 0xAA, 0xCD, 0xB6, 0xF6, 0x7C, 0xFC, 0x33, 0x05, 0x14, 0x96, 0x15, 0xC9, 0x9E,
|
||||
0x35, 0x5C, 0x7E, 0x44, 0x54, 0x58, 0x3B, 0x40, 0x20, 0xA8, 0x8B, 0x5E, 0x4A, 0x24, 0x99, 0x8E,
|
||||
0xF5, 0xB5, 0x62, 0x00, 0x37, 0x5B, 0x18, 0x65, 0x8D, 0x32, 0xE2, 0xF9, 0xDA, 0x8A, 0xD4, 0xCC,
|
||||
0x26, 0xF2, 0xF1, 0xE7, 0x4B, 0xC6, 0xCF, 0xFF, 0x4C, 0x84, 0x61, 0xFA, 0xB8, 0x0C, 0xA9, 0x50
|
||||
]
|
||||
|
||||
fixed = [
|
||||
0x67, 0x23, 0xFE, 0x30, 0x45, 0x33, 0xF8, 0x90, 0x99, 0x21, 0x07, 0xC1, 0xD0, 0x12, 0xB2, 0xA1, 0x07, 0x81
|
||||
]
|
||||
|
||||
|
||||
def gcd(a, b):
|
||||
while b > 0:
|
||||
a, b = b, a % b
|
||||
return a
|
||||
|
||||
def lcm(a, b):
|
||||
if not(a) or not(b):
|
||||
return 1
|
||||
return a * b / gcd(a, b)
|
||||
|
||||
|
||||
def UpdateHash(db, fwid):
|
||||
# extract dbid, zero out all hash stuff and add hash indicator
|
||||
dbid = db[24:32]
|
||||
hash2 = db[50:70]
|
||||
z = 20 * "\0"
|
||||
db = db[:24] + (8 * "\0") + db[32:48] + "\x01\x00" + z + db[70:88] + z + db[108:]
|
||||
|
||||
# convert fwid to byte array
|
||||
fwid = [int(fwid[i:i+2], 16) for i in xrange(0, 16, 2)]
|
||||
|
||||
# key generation, step 1: take LCM of each two bytes in the FWID in turn
|
||||
key = 16 * [0]
|
||||
for i in (0, 2, 4, 6):
|
||||
l = lcm(*fwid[i:i+2])
|
||||
hi = (l & 0xFF00) >> 8
|
||||
lo = l & 0x00FF
|
||||
j = i << 1
|
||||
key[j] = ((table1[hi] * 0xB5) - 0x03) & 0xFF
|
||||
key[j|1] = ((table2[hi] * 0xB7) + 0x49) & 0xFF
|
||||
key[j|2] = ((table1[lo] * 0xB5) - 0x03) & 0xFF
|
||||
key[j|3] = ((table2[lo] * 0xB7) + 0x49) & 0xFF
|
||||
# step 2: invert key
|
||||
key = [inv[x] for x in key]
|
||||
# step 3: create hash key
|
||||
key = fixed + key
|
||||
key = map(ord, sha.new("".join(map(chr, key))).digest())
|
||||
|
||||
# first XOR
|
||||
key = [(x ^ 0x36) for x in key] + 44 * [0x36]
|
||||
|
||||
# first SHA
|
||||
h = sha.new("".join(map(chr, key)) + db).digest()
|
||||
|
||||
# second XOR
|
||||
key = [(x ^ (0x36 ^ 0x5C)) for x in key]
|
||||
|
||||
# second SHA
|
||||
h = sha.new("".join(map(chr, key)) + h).digest()
|
||||
|
||||
# reassemble database
|
||||
return db[:24] + dbid + db[32:50] + hash2 + db[70:88] + h + db[108:]
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
fwids = GetFWIDs()
|
||||
print "detected FWIDs:", fwids
|
||||
if fwids:
|
||||
fwid = fwids[0]
|
||||
else:
|
||||
fwid = "000A27001B3EAD37"
|
||||
print "no FWID detected, using default FWID for BIST"
|
||||
|
||||
try:
|
||||
old = file(sys.argv[1], "rb").read()
|
||||
except IndexError:
|
||||
sys.exit(0)
|
||||
new = UpdateHash(old, "000A27001B3EAD37")
|
||||
print "old =>", " ".join(["%02X" % ord(c) for c in old[88:108]])
|
||||
print "new =>", " ".join(["%02X" % ord(c) for c in new[88:108]])
|
||||
if old == new:
|
||||
print "MATCH!"
|
||||
else:
|
||||
print "no match :("
|
||||
try:
|
||||
file(sys.argv[2], "wb").write(new)
|
||||
except IndexError:
|
||||
pass
|
||||
1035
iTunesDB.py
Normal file
1035
iTunesDB.py
Normal file
File diff suppressed because it is too large
Load Diff
607
mp3info.py
Normal file
607
mp3info.py
Normal file
@@ -0,0 +1,607 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# audio file information library for rePear, the iPod database management tool
|
||||
# Copyright (C) 2006-2008 Martin J. Fiedler <martin.fiedler@gmx.net>
|
||||
#
|
||||
# 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
|
||||
|
||||
import sys, re, zlib, struct, os, stat
|
||||
import qtparse
|
||||
|
||||
|
||||
################################################################################
|
||||
## a sh*tload of constants ##
|
||||
################################################################################
|
||||
|
||||
ID3v1Genres = { 0:"Blues", 1:"Classic Rock", 2:"Country", 3:"Dance", 4:"Disco",
|
||||
5:"Funk", 6:"Grunge", 7:"Hip-Hop", 8:"Jazz", 9:"Metal", 10:"New Age",
|
||||
11:"Oldies", 12:"Other", 13:"Pop", 14:"R&B", 15:"Rap", 16:"Reggae", 17:"Rock",
|
||||
18:"Techno", 19:"Industrial", 20:"Alternative", 21:"Ska", 22:"Death Metal",
|
||||
23:"Pranks", 24:"Soundtrack", 25:"Euro-Techno", 26:"Ambient", 27:"Trip-Hop",
|
||||
28:"Vocal", 29:"Jazz&Funk", 30:"Fusion", 31:"Trance", 32:"Classical",
|
||||
33:"Instrumental", 34:"Acid", 35:"House", 36:"Game", 37:"Sound Clip",
|
||||
38:"Gospel", 39:"Noise", 40:"Alternative Rock", 41:"Bass", 42:"Soul",
|
||||
43:"Punk", 44:"Space", 45:"Meditative", 46:"Instrumental Pop",
|
||||
47:"Instrumental Rock", 48:"Ethnic", 49:"Gothic", 50:"Darkwave",
|
||||
51:"Techno-Industrial", 52:"Electronic", 53:"Pop-Folk", 54:"Eurodance",
|
||||
55:"Dream", 56:"Southern Rock", 57:"Comedy", 58:"Cult", 59:"Gangsta",
|
||||
60:"Top 40", 61:"Christian Rap", 62:"Pop/Funk", 63:"Jungle", 64:"Native US",
|
||||
65:"Cabaret", 66:"New Wave", 67:"Psychedelic", 68:"Rave", 69:"Showtunes",
|
||||
70:"Trailer", 71:"Lo-Fi", 72:"Tribal", 73:"Acid Punk", 74:"Acid Jazz",
|
||||
75:"Polka", 76:"Retro", 77:"Musical", 78:"Rock & Roll", 79:"Hard Rock",
|
||||
80:"Folk", 81:"Folk-Rock", 82:"National Folk", 83:"Swing", 84:"Fast Fusion",
|
||||
85:"Bebop", 86:"Latin", 87:"Revival", 88:"Celtic", 89:"Bluegrass",
|
||||
90:"Avantgarde", 91:"Gothic Rock", 92:"Progressive Rock",
|
||||
93:"Psychedelic Rock", 94:"Symphonic Rock", 95:"Slow Rock", 96:"Big Band",
|
||||
97:"Chorus", 98:"Easy Listening", 99:"Acoustic", 100:"Humour", 101:"Speech",
|
||||
102:"Chanson", 103:"Opera", 104:"Chamber Music", 105:"Sonata", 106:"Symphony",
|
||||
107:"Booty Bass", 108:"Primus", 109:"Porn Groove", 110:"Satire",
|
||||
111:"Slow Jam", 112:"Club", 113:"Tango", 114:"Samba", 115:"Folklore",
|
||||
116:"Ballad", 117:"Power Ballad", 118:"Rhytmic Soul", 119:"Freestyle",
|
||||
120:"Duet", 121:"Punk Rock", 122:"Drum Solo", 123:"Acapella", 124:"Euro-House",
|
||||
125:"Dance Hall", 126:"Goa", 127:"Drum & Bass", 128:"Club-House",
|
||||
129:"Hardcore", 130:"Terror", 131:"Indie", 132:"BritPop", 133:"Negerpunk",
|
||||
134:"Polsk Punk", 135:"Beat", 136:"Christian Gangsta", 137:"Heavy Metal",
|
||||
138:"Black Metal", 139:"Crossover", 140:"Contemporary Christian",
|
||||
141:"Christian Rock", 142:"Merengue", 143:"Salsa", 144:"Thrash Metal",
|
||||
145:"Anime", 146:"JPop", 147:"SynthPop" }
|
||||
|
||||
|
||||
ID3v2FrameMap = {
|
||||
"TIT1": "content group",
|
||||
"TIT2": "title",
|
||||
"TIT3": "subtitle",
|
||||
"TALB": "album",
|
||||
"TOAL": "original album",
|
||||
"TRCK": "/track number/total tracks",
|
||||
"TPOS": "/disc number/total discs",
|
||||
"TPE1": "artist",
|
||||
"TPE2": "band",
|
||||
"TPE3": "conductor",
|
||||
"TPE4": "interpreted by",
|
||||
"TOPE": "original artist",
|
||||
"TEXT": "lyrics",
|
||||
"TOLY": "original lyrics",
|
||||
"TCOM": "composer",
|
||||
"TMCL": "musician credits",
|
||||
"TIPL": "involved people",
|
||||
"TENC": "encoded by",
|
||||
"TBPM": "#BPM",
|
||||
"TYER": "#year",
|
||||
# "TLEN": "length", # unreliable, rather use Xing/FhG tags or scan the file
|
||||
"TKEY": "initial key",
|
||||
"TLAN": "language",
|
||||
"TCON": "genre",
|
||||
"TFLT": "file type",
|
||||
"TMED": "media type",
|
||||
"TMOO": "mood",
|
||||
"TCOP": "copyright",
|
||||
"TPRO": "produced",
|
||||
"TPUB": "publisher",
|
||||
"TOWN": "owner",
|
||||
"TRSN": "station name",
|
||||
"TRSO": "station owner",
|
||||
"TOFN": "original file name",
|
||||
"TDLY": "playlist delay",
|
||||
"TDEN": "encoding time",
|
||||
"TDOR": "original release time",
|
||||
"TDRC": "recording time",
|
||||
"TDRL": "release time",
|
||||
"TDTG": "tagging time",
|
||||
"TSSE": "encoding settings",
|
||||
"TSOA": "album sort order",
|
||||
"TSOP": "performer sort order",
|
||||
"TSOT": "title sort order",
|
||||
"WCOM": "commercial information URL",
|
||||
"WCOP": "copyright URL",
|
||||
"WOAF": "audio file URL",
|
||||
"WOAR": "artist URL",
|
||||
"WOAS": "audio source URL",
|
||||
"WORS": "station URL",
|
||||
"WPAY": "payment URL",
|
||||
"WPUB": "publisher URL",
|
||||
"COMM": "comment"
|
||||
}
|
||||
|
||||
RE_ID3v2_Frame_Type = re.compile(r'[A-Z0-9]{4}')
|
||||
RE_ID3v2_Strip_Genre = re.compile(r'\([0-9]+\)(.*)')
|
||||
|
||||
|
||||
################################################################################
|
||||
## ID3v1 decoder ##
|
||||
################################################################################
|
||||
|
||||
def GetID3v1(f, info):
|
||||
try:
|
||||
f.seek(-128, 2)
|
||||
data = f.read(128)
|
||||
except IOError:
|
||||
return 0
|
||||
if len(data)!=128 or data[:3]!="TAG":
|
||||
return 0
|
||||
info['tag'] = "id3v1"
|
||||
field = data[3:33].split("\0",1)[0].strip()
|
||||
if field: info['title'] = unicode(field, sys.getfilesystemencoding(), 'replace')
|
||||
field = data[33:63].split("\0",1)[0].strip()
|
||||
if field: info['artist'] = unicode(field, sys.getfilesystemencoding(), 'replace')
|
||||
field = data[63:93].split("\0",1)[0].strip()
|
||||
if field: info['album'] = unicode(field, sys.getfilesystemencoding(), 'replace')
|
||||
field = data[93:97].split("\0",1)[0].strip()
|
||||
if field:
|
||||
try:
|
||||
info['year'] = int(field)
|
||||
except ValueError:
|
||||
pass
|
||||
field = data[97:127].split("\0",1)[0].strip()
|
||||
if field: info['comment'] = unicode(field, sys.getfilesystemencoding(), 'replace')
|
||||
if data[125]=='\0' and data[126]!='\0':
|
||||
info['track number'] = ord(data[126])
|
||||
try:
|
||||
info['genre'] = ID3v1Genres[ord(data[127])]
|
||||
except KeyError:
|
||||
pass
|
||||
return -128
|
||||
|
||||
|
||||
################################################################################
|
||||
## ID3v2 decoder ##
|
||||
################################################################################
|
||||
|
||||
def DecodeInteger(s):
|
||||
res = 0
|
||||
for c in s:
|
||||
res = (res << 8) | ord(c)
|
||||
return res
|
||||
|
||||
def DecodeSyncsafeInteger(s):
|
||||
res = 0
|
||||
for c in s:
|
||||
res = (res << 7) | (ord(c) & 0x7F)
|
||||
return res
|
||||
|
||||
|
||||
def GetCharset(encoding):
|
||||
if encoding=="\1": return "utf_16"
|
||||
if encoding=="\2": return "utf_16_be"
|
||||
if encoding=="\3": return "utf_8"
|
||||
else: return "iso-8859-1"
|
||||
|
||||
|
||||
def GetEndID3v2(f, offset=0):
|
||||
try:
|
||||
f.seek(offset-10, 2)
|
||||
marker = f.read(10)
|
||||
if len(marker)!=10 or marker[:3]!="3DI":
|
||||
return None
|
||||
size = DecodeSyncsafeInteger(marker[-4:]) + 10
|
||||
f.seek(offset-10-size, 2)
|
||||
data = f.read(size)
|
||||
if len(data)!=size or data[:3]!="ID3":
|
||||
return None
|
||||
return data
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
|
||||
def GetStartID3v2(f):
|
||||
try:
|
||||
f.seek(0)
|
||||
marker = f.read(10)
|
||||
if len(marker)!=10 or marker[:3]!="ID3":
|
||||
return None
|
||||
size = DecodeSyncsafeInteger(marker[-4:])
|
||||
payload = f.read(size)
|
||||
if len(payload)!=size:
|
||||
return None
|
||||
return marker+payload
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
|
||||
def DecodeID3v2(data, info):
|
||||
info['tag'] = "id3v2.%d.%d" % (ord(data[3]), ord(data[4]))
|
||||
if ord(data[3]) >= 4:
|
||||
decode_size = DecodeSyncsafeInteger
|
||||
else:
|
||||
decode_size = DecodeInteger
|
||||
|
||||
# parse header flags, strip header(s)
|
||||
flags = ord(data[5])
|
||||
data = data[10:]
|
||||
if flags & 0x40: # extended header
|
||||
size = decode_size(data[:4])
|
||||
data = data[size:]
|
||||
|
||||
# parse frames
|
||||
while len(data)>=10:
|
||||
frame = data[:4]
|
||||
if not RE_ID3v2_Frame_Type.match(frame):
|
||||
break # invalid frame name or start of padding => bail out
|
||||
size = decode_size(data[4:8])
|
||||
payload = data[10:size+10]
|
||||
flags = ord(data[9])
|
||||
if flags & 0x02:
|
||||
payload = payload.replace("\xff\0", "\xff")
|
||||
if flags & 0x04:
|
||||
try:
|
||||
payload = zlib.decompress(payload)
|
||||
except zlib.error:
|
||||
continue # this frame is broken
|
||||
HandleID3v2Frame(frame, payload, flags, info)
|
||||
data = data[size+10:]
|
||||
|
||||
|
||||
def HandleID3v2Frame(frame, payload, flags, info):
|
||||
text = None
|
||||
if not payload: return # empty payload
|
||||
|
||||
if frame[0]=='T' and frame!="TXXX":
|
||||
# text frame
|
||||
charset = GetCharset(payload[0])
|
||||
text = unicode(payload[1:], charset, 'replace').split(u'\0', 1)[0]
|
||||
|
||||
elif frame[0]=='W' and frame!="WXXX":
|
||||
# URL
|
||||
text = unicode(payload.split("\0", 1)[0], "iso-8859-1", 'replace')
|
||||
|
||||
elif frame=="COMM":
|
||||
# comment
|
||||
charset = GetCharset(payload[0])
|
||||
lang = payload[1:4].split("\0", 1)[0]
|
||||
parts = unicode(payload[4:], charset, 'replace').split(u'\0', 2)
|
||||
if len(parts)<2: return # broken frame
|
||||
text = parts[1]
|
||||
|
||||
if text: ##### apply the current textual frame ####
|
||||
key = ID3v2FrameMap.get(frame, frame)
|
||||
text = text.strip()
|
||||
|
||||
if frame=="TCON": # strip crappy numerical genre comment
|
||||
m = RE_ID3v2_Strip_Genre.match(text.encode('iso-8859-1', 'replace'))
|
||||
if m: text = m.group(1)
|
||||
|
||||
if key[0]=="#": # numerical key
|
||||
try:
|
||||
text = int(text.strip())
|
||||
except ValueError:
|
||||
return # broken frame
|
||||
key = key[1:]
|
||||
|
||||
if key[0]=="/": # multipart numerical key
|
||||
keys = key[1:].split("/")
|
||||
values = text.split("/")
|
||||
for key, value in zip(keys, values):
|
||||
try:
|
||||
info[key] = int(value)
|
||||
except:
|
||||
pass
|
||||
return # already done here
|
||||
|
||||
info[key] = text
|
||||
|
||||
|
||||
################################################################################
|
||||
## ultra-simple (and not very fault-tolerant) Ogg Vorbis metadata decoder ##
|
||||
################################################################################
|
||||
|
||||
def DecodeVorbisHeader(f, info):
|
||||
try:
|
||||
f.seek(0)
|
||||
data = f.read(4096) # almost one page, should be enough
|
||||
except IOError:
|
||||
return False
|
||||
if data[:4]!="OggS": return False # no Ogg -- don't bother
|
||||
data = data.split("vorbis", 3)
|
||||
if len(data)!=4: return False # no Vorbis packets
|
||||
info['format'] = "ogg" # at this point, we can assume the stream is valid
|
||||
info['filetype'] = "Ogg Vorbis"
|
||||
data = data[2]
|
||||
if len(data)<8: return True # comment packet too short
|
||||
|
||||
# encoder version
|
||||
size = struct.unpack("<L", data[:4])[0]
|
||||
if size: info['encoder'] = unicode(data[4:size+4], "utf_8", 'replace')
|
||||
data = data[size+4:]
|
||||
|
||||
# field count
|
||||
if len(data)<8: return True # comment packet too short
|
||||
count = struct.unpack("<L", data[:4])[0]
|
||||
data = data[4:]
|
||||
|
||||
# field data
|
||||
for i in xrange(count):
|
||||
if len(data)<4: break # comment packet too short
|
||||
size = struct.unpack("<L", data[:4])[0]
|
||||
if size:
|
||||
line = data[4:size+4]
|
||||
if "=" in line:
|
||||
key, value = line.split('=', 1)
|
||||
value = value.strip()
|
||||
if key=="TRACKNUMBER":
|
||||
try:
|
||||
info["track number"] = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
info[key.lower()] = unicode(value, "utf_8", 'replace')
|
||||
data = data[size+4:]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
################################################################################
|
||||
## MP3 frame parser ##
|
||||
################################################################################
|
||||
|
||||
mp3_bitrates = [
|
||||
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,0],
|
||||
[0, 32, 40, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,0]
|
||||
]
|
||||
|
||||
mp3_samplerates = [
|
||||
[22050,24000,16000,0x0FFFFFFF],
|
||||
[44100,48000,32000,0x0FFFFFFF]
|
||||
]
|
||||
|
||||
|
||||
def IsValidMP3Header(header):
|
||||
return (len(header) == 4) \
|
||||
and ( header[0] == 0xFF) \
|
||||
and ((header[1] & 0xF0) == 0xF0) \
|
||||
and ((header[1] & 0x06) == 0x02) \
|
||||
and ((header[2] & 0xF0) != 0xF0) \
|
||||
and (header[2] & 0xF0) \
|
||||
and ((header[2] & 0x0C) != 0x0C)
|
||||
|
||||
|
||||
def ScanMP3(f, info, start_offset=0):
|
||||
try:
|
||||
f.seek(start_offset)
|
||||
sample = f.read(64*1024) # the MP3 stuff should start in the first 64k
|
||||
|
||||
# search for the start of the MP3 data part
|
||||
pos = 0
|
||||
while True:
|
||||
pos = sample.find("\xff", pos)
|
||||
if pos<0: return False
|
||||
if IsValidMP3Header(map(ord, sample[pos:pos+4])): break
|
||||
pos += 1
|
||||
f.seek(start_offset + pos)
|
||||
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
# init global statistics
|
||||
total_samples = 0
|
||||
total_frames = 0
|
||||
total_bytes = 0
|
||||
used_bitrates = {}
|
||||
force_vbr = False
|
||||
data = ""
|
||||
|
||||
# scan the file
|
||||
while True:
|
||||
try:
|
||||
header=f.read(4)
|
||||
except IOError:
|
||||
break
|
||||
if len(header)!=4: break
|
||||
|
||||
# reject frames that do not look like MP3
|
||||
header = map(ord, header)
|
||||
if not IsValidMP3Header(header):
|
||||
# OK, this file is broken. try to re-synchronize.
|
||||
resync_pos = f.tell()
|
||||
try:
|
||||
# search for the first 8 bits of a frame sync marker in the
|
||||
# next 4 KiB
|
||||
pos = f.read(4096).find("\xff")
|
||||
except IOError:
|
||||
break
|
||||
if pos < 0:
|
||||
break
|
||||
else:
|
||||
f.seek(resync_pos + pos)
|
||||
continue
|
||||
|
||||
# calculate details
|
||||
version = (header[1]>>3) & 1
|
||||
samples = 576 * (version+1)
|
||||
b2 = header[2]
|
||||
bitrate = mp3_bitrates[version][b2>>4]
|
||||
samplerate = mp3_samplerates[version][(b2>>2) & 3]
|
||||
padding = (b2>>1) & 1
|
||||
framesize = 72000 * (version+1) * bitrate / samplerate + padding
|
||||
|
||||
# skip frame data
|
||||
try:
|
||||
frame = f.read(framesize-4)
|
||||
# accumulate the data of the first 10 frames
|
||||
if total_frames < 10:
|
||||
data += frame
|
||||
except IOError:
|
||||
break
|
||||
|
||||
# fix statistics
|
||||
total_samples += samples
|
||||
total_frames += 1
|
||||
total_bytes += framesize
|
||||
used_bitrates[bitrate] = None
|
||||
|
||||
# after 10 frames, check for Xing/LAME/FhG headers
|
||||
if total_frames == 10:
|
||||
valid = False
|
||||
# check for Xing/LAME VBR header
|
||||
p2 = data.find("Xing\0\0\0")
|
||||
if (p2 > 0) and (ord(data[p2 + 7]) & 1):
|
||||
force_vbr = True
|
||||
# check for LAME CBR header
|
||||
p = data.find("Info\0\0\0")
|
||||
if force_vbr or ((p > 0) and (ord(data[p + 7]) & 1)):
|
||||
if force_vbr: p = p2
|
||||
total_frames, total_bytes = struct.unpack(">ii", data[p+8:p+16])
|
||||
if not(ord(data[p + 7]) & 2):
|
||||
total_bytes = info['size'] # size not specified, estimate
|
||||
total_samples = total_frames * samples
|
||||
valid = True
|
||||
# check for FhG header
|
||||
else:
|
||||
p = data.find("VBRI\0\1")
|
||||
if p > 0:
|
||||
force_vbr = True
|
||||
total_bytes, total_frames = struct.unpack(">ii", data[p+10:p+18])
|
||||
total_samples = total_frames * samples
|
||||
valid = True
|
||||
# final sanity check
|
||||
if valid:
|
||||
if (total_frames < 10) or (total_bytes < 1000) or (total_bytes > info['size']):
|
||||
valid = False
|
||||
if valid:
|
||||
# verify computed bitrate
|
||||
check_bitrate = total_bytes*8*0.001/total_frames/samples*samplerate
|
||||
if force_vbr:
|
||||
# valid range for VBR files: all the way through
|
||||
min_rate = 30.0
|
||||
max_rate = 330.0
|
||||
else:
|
||||
# valid range for CBR files: current bitrate +/- 10%
|
||||
min_rate = bitrate * 0.9
|
||||
max_rate = bitrate * 1.1
|
||||
valid = (check_bitrate > min_rate) and (check_bitrate < max_rate)
|
||||
if valid:
|
||||
break
|
||||
else:
|
||||
# this didn't work out, continue conventionally
|
||||
total_samples = 10 * samples
|
||||
total_frames = 10
|
||||
total_bytes = 0
|
||||
force_vbr = False
|
||||
|
||||
# scan complete, finish things
|
||||
if total_frames < 10:
|
||||
return False # less than 10 frames? that's a little bit short ...
|
||||
info['filetype'] = "MPEG-%d Audio Layer 3" % (2-version)
|
||||
info['sample rate'] = samplerate
|
||||
info['sample count'] = total_samples
|
||||
info['length'] = total_samples * 1.0 / samplerate
|
||||
if force_vbr or (len(used_bitrates) > 1):
|
||||
info['format'] = "mp3-vbr"
|
||||
info['bitrate'] = int(total_bytes*8*0.001 / info['length'])
|
||||
else:
|
||||
info['format'] = "mp3-cbr"
|
||||
info['bitrate'] = bitrate
|
||||
return True
|
||||
|
||||
|
||||
################################################################################
|
||||
## MP4 decoder wrapper ##
|
||||
################################################################################
|
||||
|
||||
def DecodeMP4(f, info):
|
||||
try:
|
||||
f.seek(0)
|
||||
first_atom = f.read(8)[4:]
|
||||
except IOError:
|
||||
return False
|
||||
if not(first_atom in ('moov', 'ftyp')):
|
||||
return False # no MP4 file
|
||||
try:
|
||||
qt = qtparse.QTParser(f)
|
||||
except IOError:
|
||||
return False
|
||||
info.update(qt.get_repear_info())
|
||||
del qt
|
||||
return True
|
||||
|
||||
|
||||
################################################################################
|
||||
## toplevel GetAudioFileInfo() function ##
|
||||
################################################################################
|
||||
|
||||
def GetAudioFileInfo(filename, stat_only=False):
|
||||
try:
|
||||
s = os.stat(filename)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
if not stat.S_ISREG(s[stat.ST_MODE]):
|
||||
return None
|
||||
info = {'path': filename, 'size':s[stat.ST_SIZE], 'mtime':s[stat.ST_MTIME]}
|
||||
if stat_only: return info
|
||||
|
||||
# try to extract a track number from the file name
|
||||
track = 0
|
||||
for c in os.path.split(filename)[-1]:
|
||||
if c in "0123456789":
|
||||
track = (10 * track) + ord(c) - 48
|
||||
else:
|
||||
break
|
||||
if track:
|
||||
info['track number'] = track
|
||||
|
||||
# open the file
|
||||
try:
|
||||
f = file(filename, "rb")
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
# MP4 probing
|
||||
if DecodeMP4(f, info):
|
||||
return info
|
||||
|
||||
# Ogg Vorbis probing
|
||||
if DecodeVorbisHeader(f, info):
|
||||
return info
|
||||
|
||||
# some ID3 probing
|
||||
end_offset = GetID3v1(f, info)
|
||||
id3v2_data = GetEndID3v2(f, end_offset)
|
||||
if id3v2_data:
|
||||
DecodeID3v2(id3v2_data, info)
|
||||
id3v2_data = GetStartID3v2(f)
|
||||
if id3v2_data:
|
||||
start_offset = len(id3v2_data)
|
||||
DecodeID3v2(id3v2_data, info)
|
||||
else:
|
||||
start_offset = 0
|
||||
ScanMP3(f, info, start_offset)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
################################################################################
|
||||
## a demo main function ##
|
||||
################################################################################
|
||||
|
||||
if __name__=="__main__":
|
||||
if len(sys.argv)<2:
|
||||
print "Usage:", sys.argv[0], "<FILES>..."
|
||||
sys.exit(1)
|
||||
for filename in sys.argv[1:]:
|
||||
print
|
||||
print "[%s]" % filename
|
||||
info = GetAudioFileInfo(filename)
|
||||
if not info: continue
|
||||
keys = info.keys()
|
||||
keys.sort()
|
||||
fmt = "%%-%ds= %%s" % (max(map(len, keys)) + 1)
|
||||
for key in keys:
|
||||
value = info[key]
|
||||
try:
|
||||
value = value.encode('iso-8859-1', 'replace')
|
||||
except:
|
||||
pass
|
||||
print fmt % (key, value)
|
||||
print
|
||||
581
qtparse.py
Normal file
581
qtparse.py
Normal file
@@ -0,0 +1,581 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# QuickTime parser library for rePear, the iPod database management tool
|
||||
# Copyright (C) 2006-2008 Martin J. Fiedler <martin.fiedler@gmx.net>
|
||||
#
|
||||
# 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
|
||||
|
||||
import sys, struct, types
|
||||
|
||||
ID3v1Genres = { 0:"Blues", 1:"Classic Rock", 2:"Country", 3:"Dance", 4:"Disco",
|
||||
5:"Funk", 6:"Grunge", 7:"Hip-Hop", 8:"Jazz", 9:"Metal", 10:"New Age",
|
||||
11:"Oldies", 12:"Other", 13:"Pop", 14:"R&B", 15:"Rap", 16:"Reggae", 17:"Rock",
|
||||
18:"Techno", 19:"Industrial", 20:"Alternative", 21:"Ska", 22:"Death Metal",
|
||||
23:"Pranks", 24:"Soundtrack", 25:"Euro-Techno", 26:"Ambient", 27:"Trip-Hop",
|
||||
28:"Vocal", 29:"Jazz&Funk", 30:"Fusion", 31:"Trance", 32:"Classical",
|
||||
33:"Instrumental", 34:"Acid", 35:"House", 36:"Game", 37:"Sound Clip",
|
||||
38:"Gospel", 39:"Noise", 40:"Alternative Rock", 41:"Bass", 42:"Soul",
|
||||
43:"Punk", 44:"Space", 45:"Meditative", 46:"Instrumental Pop",
|
||||
47:"Instrumental Rock", 48:"Ethnic", 49:"Gothic", 50:"Darkwave",
|
||||
51:"Techno-Industrial", 52:"Electronic", 53:"Pop-Folk", 54:"Eurodance",
|
||||
55:"Dream", 56:"Southern Rock", 57:"Comedy", 58:"Cult", 59:"Gangsta",
|
||||
60:"Top 40", 61:"Christian Rap", 62:"Pop/Funk", 63:"Jungle", 64:"Native US",
|
||||
65:"Cabaret", 66:"New Wave", 67:"Psychedelic", 68:"Rave", 69:"Showtunes",
|
||||
70:"Trailer", 71:"Lo-Fi", 72:"Tribal", 73:"Acid Punk", 74:"Acid Jazz",
|
||||
75:"Polka", 76:"Retro", 77:"Musical", 78:"Rock & Roll", 79:"Hard Rock",
|
||||
80:"Folk", 81:"Folk-Rock", 82:"National Folk", 83:"Swing", 84:"Fast Fusion",
|
||||
85:"Bebop", 86:"Latin", 87:"Revival", 88:"Celtic", 89:"Bluegrass",
|
||||
90:"Avantgarde", 91:"Gothic Rock", 92:"Progressive Rock",
|
||||
93:"Psychedelic Rock", 94:"Symphonic Rock", 95:"Slow Rock", 96:"Big Band",
|
||||
97:"Chorus", 98:"Easy Listening", 99:"Acoustic", 100:"Humour", 101:"Speech",
|
||||
102:"Chanson", 103:"Opera", 104:"Chamber Music", 105:"Sonata", 106:"Symphony",
|
||||
107:"Booty Bass", 108:"Primus", 109:"Porn Groove", 110:"Satire",
|
||||
111:"Slow Jam", 112:"Club", 113:"Tango", 114:"Samba", 115:"Folklore",
|
||||
116:"Ballad", 117:"Power Ballad", 118:"Rhytmic Soul", 119:"Freestyle",
|
||||
120:"Duet", 121:"Punk Rock", 122:"Drum Solo", 123:"Acapella", 124:"Euro-House",
|
||||
125:"Dance Hall", 126:"Goa", 127:"Drum & Bass", 128:"Club-House",
|
||||
129:"Hardcore", 130:"Terror", 131:"Indie", 132:"BritPop", 133:"Negerpunk",
|
||||
134:"Polsk Punk", 135:"Beat", 136:"Christian Gangsta", 137:"Heavy Metal",
|
||||
138:"Black Metal", 139:"Crossover", 140:"Contemporary Christian",
|
||||
141:"Christian Rock", 142:"Merengue", 143:"Salsa", 144:"Thrash Metal",
|
||||
145:"Anime", 146:"JPop", 147:"SynthPop" }
|
||||
|
||||
|
||||
|
||||
QTAtomTypeMap = {
|
||||
'moov': 'container',
|
||||
'udta': 'container',
|
||||
'trak': 'container',
|
||||
'mdia': 'container',
|
||||
'minf': 'container',
|
||||
'stbl': 'container',
|
||||
'pinf': 'container',
|
||||
'schi': 'container',
|
||||
'ilst': 'container',
|
||||
}
|
||||
|
||||
QTTrackTypeMap = {
|
||||
'vide': 'video',
|
||||
'soun': 'audio',
|
||||
}
|
||||
|
||||
QTMetaDataMap = {
|
||||
'$nam': ('text', 'title'),
|
||||
'$alb': ('text', 'album'),
|
||||
'$art': ('text', 'artist'),
|
||||
'$ART': ('text', 'artist'), # FAAC writes this tag in captital letters
|
||||
'aART': ('text', 'album artist'),
|
||||
'$cmt': ('text', 'comment'),
|
||||
'$day': ('year', 'year'),
|
||||
'$gen': ('text', 'genre'),
|
||||
'$wrt': ('text', 'composer'),
|
||||
'$too': ('text', 'encoder'),
|
||||
'cprt': ('text', 'copyright'),
|
||||
'trkn': ('track', None),
|
||||
'disk': ('disc', None),
|
||||
'covr': ('artwork', None),
|
||||
'cpil': ('flag', 'compilation'),
|
||||
'$lyr': ('text', 'lyrics'),
|
||||
'desc': ('text', 'description'),
|
||||
'purl': ('text', 'podcast url'),
|
||||
'egid': ('text', 'episode id'),
|
||||
'catg': ('text', 'category'),
|
||||
'keyw': ('text', 'keyword'),
|
||||
'gnre': ('genre', None),
|
||||
# gnre Genre 1 | 0 text | uint8 iTunes 4.0
|
||||
# tmpo BPM 21 uint8 iTunes 4.0
|
||||
# rtng Rating/Advisory 21 uint8 iTunes 4.0
|
||||
# stik ?? (stik) 21 uint8 ??
|
||||
# pcst Podcast 21 uint8 iTunes 4.9
|
||||
# tvnn TV Network Name 1 text iTunes 6.0
|
||||
# tvsh TV Show Name 1 text iTunes 6.0
|
||||
# tven TV Episode No 1 text iTunes 6.0
|
||||
# tvsn TV Season 21 uint8 iTunes 6.0
|
||||
# tves TV Episode 21 uint8 iTunes 6.0
|
||||
# pgap Gapless Play 21 uin8 iTunes 7.0
|
||||
}
|
||||
|
||||
MP4DescriptorMap = {
|
||||
0x03: 'MP4ESDescr',
|
||||
0x04: 'MP4DecConfigDescr',
|
||||
0x05: 'MP4DecSpecificDescr',
|
||||
0x06: 'MP4SLConfigDescr',
|
||||
}
|
||||
|
||||
MP4ObjectTypeMap = {
|
||||
0x20: 'MPEG4Visual',
|
||||
0x40: 'MPEG4Audio',
|
||||
}
|
||||
|
||||
MP4ProfileMap = {
|
||||
1: "AAC Main",
|
||||
2: "LC-AAC",
|
||||
3: "AAC SSR",
|
||||
4: "AAC LTP",
|
||||
5: "HE-AAC",
|
||||
6: "Scalable",
|
||||
7: "TwinVQ",
|
||||
8: "CELP",
|
||||
9: "HVXC",
|
||||
12: "TTSI",
|
||||
13: "Main Synthetic Profile",
|
||||
14: "Wavetable synthesis",
|
||||
15: "General MIDI",
|
||||
16: "Algorithmic Synthesis and Audio FX",
|
||||
17: "LC-AAC with error recovery",
|
||||
19: "AAC LTP with error recovery",
|
||||
20: "AAC SSR with error recovery",
|
||||
21: "TwinVQ with error recovery",
|
||||
22: "BSAC with error recovery",
|
||||
23: "AAC LD with error recovery",
|
||||
24: "CELP with error recovery",
|
||||
25: "HXVC with error recovery",
|
||||
26: "HILN with error recovery",
|
||||
27: "Parametric with error recovery",
|
||||
}
|
||||
|
||||
H264ProfileMap = {
|
||||
66: "BP",
|
||||
77: "MP",
|
||||
88: "EP",
|
||||
100: "HP",
|
||||
110: "H10P",
|
||||
144: "H444P",
|
||||
}
|
||||
|
||||
|
||||
def chop(s):
|
||||
if s: return (ord(s[0]), s[1:])
|
||||
return (0, "")
|
||||
|
||||
def dictremove(d, rlist):
|
||||
for r in rlist:
|
||||
if r in d:
|
||||
del d[r]
|
||||
|
||||
|
||||
class QTParser:
|
||||
def __init__(self, f, verbose=False):
|
||||
self.f = f
|
||||
self.verbose = verbose
|
||||
self.info = {}
|
||||
self.time_scale = 1
|
||||
self.tracks = {}
|
||||
self.trackid = None
|
||||
self.artwork = []
|
||||
self.errors = []
|
||||
|
||||
self.f.seek(0, 2)
|
||||
self.info['size'] = self.f.tell()
|
||||
self.parse_container(0, self.info['size'])
|
||||
|
||||
def log_path(self, path, atom, size, start=None):
|
||||
if not self.verbose: return
|
||||
if start is None:
|
||||
print "%s%s (%d bytes)" % (" " * len(path), atom, size)
|
||||
else:
|
||||
print "%s%s (%d bytes @ %d)" % (" " * len(path), atom, size, start)
|
||||
|
||||
def err(self, path, message):
|
||||
self.errors.append((self.repr_path(path), message))
|
||||
|
||||
def reject(self, path, size, minsize, need_track=True):
|
||||
atom = path[-1]
|
||||
if need_track:
|
||||
if self.trackid is None:
|
||||
return self.err(path, "%s outside of a track" % atom)
|
||||
if not(self.trackid in self.tracks):
|
||||
return True
|
||||
if size < minsize:
|
||||
return self.err(path, "atom too small")
|
||||
return False
|
||||
|
||||
def gettrack(self, prop, default=None):
|
||||
return self.tracks[self.trackid].get(prop, default)
|
||||
def settrack(self, prop, value):
|
||||
self.tracks[self.trackid][prop] = value
|
||||
|
||||
def repr_path(self, path):
|
||||
if not path: return "<root>"
|
||||
return ".".join(path)
|
||||
|
||||
def parse_container(self, start=0, size=0, path=[]):
|
||||
end = start + size
|
||||
while (start + 8) < end:
|
||||
self.f.seek(start)
|
||||
head = self.f.read(8)
|
||||
start += 8
|
||||
size = struct.unpack(">L", head[:4])[0] - 8
|
||||
if size < 0:
|
||||
return self.err(path, "invalid sub-atom size")
|
||||
atom = head[4:].strip("\0 ").replace('\xa9', '$')
|
||||
if not atom:
|
||||
break
|
||||
self.log_path(path, atom, size, start)
|
||||
if atom in QTMetaDataMap:
|
||||
alias = 'container'
|
||||
else:
|
||||
alias = QTAtomTypeMap.get(atom, atom)
|
||||
try:
|
||||
parser = getattr(self, "parse_" + alias)
|
||||
except AttributeError:
|
||||
parser = None
|
||||
if parser:
|
||||
parser(start, min(size, end - start), path + [atom])
|
||||
start += size
|
||||
if start < end:
|
||||
return self.err(path, "%d orphaned bytes" % (end - start))
|
||||
if start > end:
|
||||
return self.err(path, "%d missing bytes" % (start - end))
|
||||
|
||||
def parse_trak(self, start, size, path):
|
||||
self.track = None
|
||||
self.parse_container(start, size, path)
|
||||
self.track = None
|
||||
|
||||
def parse_mvhd(self, start, size, path):
|
||||
if self.reject(path, size, 20, False): return
|
||||
data = self.f.read(20)
|
||||
self.time_scale, length = struct.unpack(">LL", data[12:])
|
||||
self.info['length'] = float(length) / self.time_scale
|
||||
|
||||
def parse_tkhd(self, start, size, path):
|
||||
if self.reject(path, size, 24, False): return
|
||||
data = self.f.read(min(size, 84))
|
||||
self.trackid, dummy, length = struct.unpack(">LLL", data[12:24])
|
||||
if not self.trackid in self.tracks:
|
||||
self.tracks[self.trackid] = {}
|
||||
self.settrack('length', float(length) / self.time_scale)
|
||||
if len(data) >= 84:
|
||||
w, h = struct.unpack(">LL", data[76:84])
|
||||
self.settrack('width', w >> 16)
|
||||
self.settrack('height', h >> 16)
|
||||
|
||||
def parse_mdhd(self, start, size, path):
|
||||
if self.reject(path, size, 20): return
|
||||
data = self.f.read(20)
|
||||
time_scale, length = struct.unpack(">LL", data[12:])
|
||||
self.settrack('length', float(length) / time_scale)
|
||||
|
||||
def parse_hdlr(self, start, size, path):
|
||||
if 'udta' in path:
|
||||
return
|
||||
if self.reject(path, size, 12): return
|
||||
data = self.f.read(12)
|
||||
try:
|
||||
self.tracks[self.trackid]['type'] = QTTrackTypeMap[data[8:]]
|
||||
except KeyError:
|
||||
del self.tracks[self.trackid]
|
||||
|
||||
def parse_stsd(self, start, size, path):
|
||||
if self.reject(path, size, 8): return
|
||||
data = self.f.read(8)
|
||||
count = struct.unpack(">L", data[4:8])[0]
|
||||
end = start + size
|
||||
start += 8
|
||||
media_type = self.gettrack('type')
|
||||
for i in xrange(count):
|
||||
if start > (end - 16):
|
||||
return self.err(path, "description #%d too small" % (i+1))
|
||||
self.f.seek(start)
|
||||
data = self.f.read(16)
|
||||
start += 16
|
||||
size = struct.unpack(">L", data[:4])[0] - 16
|
||||
format = data[4:8].strip("\0 ")
|
||||
refidx = struct.unpack(">H", data[14:])[0]
|
||||
self.log_path(path, format, size, start)
|
||||
try:
|
||||
parser = getattr(self, "parse_stsd_" + media_type)
|
||||
except KeyError:
|
||||
if not i: self.err(path, "descriptions found, but no handler defined")
|
||||
parser = None
|
||||
except AttributeError:
|
||||
parser = None
|
||||
if parser:
|
||||
self.settrack('format', format)
|
||||
parser(start, min(size, end - start), path + [format])
|
||||
start += size
|
||||
|
||||
def parse_stsd_audio(self, start, size, path):
|
||||
if self.reject(path, size, 20): return
|
||||
data = self.f.read(20)
|
||||
version, rev, ven, chan, res, compid, packsize, rate_hi, rate_lo = \
|
||||
struct.unpack(">HHLHHHHHH", data)
|
||||
if version == 0:
|
||||
hsize = 20
|
||||
elif version == 1:
|
||||
hsize = 24
|
||||
else:
|
||||
return self.err(path, "unknown audio stream description version")
|
||||
if size < hsize:
|
||||
return self.err(path, "stream description too small")
|
||||
start += hsize
|
||||
size -= hsize
|
||||
self.settrack('channels', chan)
|
||||
self.settrack('bits per sample', res)
|
||||
self.settrack('sample rate', rate_hi)
|
||||
if self.gettrack('length'):
|
||||
self.settrack('sample count', int(rate_hi * self.gettrack('length') + 0.5))
|
||||
self.parse_container(start, size, path)
|
||||
|
||||
def parse_stsd_video(self, start, size, path):
|
||||
if self.reject(path, size, 70): return
|
||||
version, rev, ven, tq, sq, w, h, hres, vres, zero, frames = \
|
||||
struct.unpack(">HHLLLHHLLLH", self.f.read(34))
|
||||
if (w != self.gettrack('width')) or (h != self.gettrack('height')):
|
||||
self.err(path, "video size doesn't match track header value")
|
||||
data = self.f.read(32)
|
||||
clen = ord(data[0])
|
||||
if clen > 31:
|
||||
self.err(path, "invalid compressor name length")
|
||||
elif clen:
|
||||
self.settrack('compressor', data[1:clen+1])
|
||||
self.parse_container(start + 70, size - 70, path)
|
||||
|
||||
def parse_avcC(self, start, size, path):
|
||||
if self.reject(path, size, 4): return
|
||||
data = self.f.read(4)
|
||||
profile = H264ProfileMap.get(ord(data[1]), None)
|
||||
level = ord(data[3])
|
||||
if level % 10:
|
||||
level = "%d.%d" % (level / 10, level % 10)
|
||||
else:
|
||||
level = str(level / 10)
|
||||
if (level == "1.1") and (ord(data[2]) & 0x10):
|
||||
level = "1b"
|
||||
format = "H.264"
|
||||
if profile: format += " " + profile
|
||||
self.settrack('video format', format + "@L" + level)
|
||||
|
||||
def parse_esds(self, start, size, path):
|
||||
try:
|
||||
if not(path[-2] in ('mp4a', 'mp4v')):
|
||||
return # unknown format, ignore it
|
||||
except IndexError:
|
||||
return self.err(path, "esds atom found at root level")
|
||||
if self.reject(path, size, 4, False): return
|
||||
self.f.seek(start + 4)
|
||||
self.parse_mp4desc(path, self.f.read(size - 4))
|
||||
|
||||
def parse_mp4desc(self, path, data):
|
||||
while data:
|
||||
tag, data = chop(data)
|
||||
size = 0
|
||||
while True:
|
||||
if not data:
|
||||
return self.err(path, "descriptor ends while decoding length")
|
||||
byte, data = chop(data)
|
||||
size = (size << 7) | (byte & 0x7F)
|
||||
if not(byte & 0x80): break
|
||||
if size > len(data):
|
||||
self.err(path, "%d missing bytes in descriptor" % (size - len(data)))
|
||||
size = len(data)
|
||||
tag = MP4DescriptorMap.get(tag, "0x%02X" % tag)
|
||||
self.log_path(path, tag, size)
|
||||
try:
|
||||
parser = getattr(self, "parse_" + tag)
|
||||
except AttributeError:
|
||||
parser = None
|
||||
if parser:
|
||||
parser(path + [tag], data[:size])
|
||||
data = data[size:]
|
||||
|
||||
def parse_MP4ESDescr(self, path, data):
|
||||
if self.reject(path, len(data), 3, False): return
|
||||
esid, flags = struct.unpack(">BH", data[:3])
|
||||
data = data[3:]
|
||||
if flags & 0x80: # stream_dependence
|
||||
if self.reject(path, len(data), 2, False): return
|
||||
data = data[2:]
|
||||
if flags & 0x40: # URL
|
||||
if self.reject(path, len(data), 1, False): return
|
||||
size, data = chop(data)
|
||||
if self.reject(path, len(data), size, False): return
|
||||
data = data[size:]
|
||||
if flags & 0x20: # ocr_stream
|
||||
if self.reject(path, len(data), 2, False): return
|
||||
data = data[2:]
|
||||
self.parse_mp4desc(path, data)
|
||||
|
||||
def parse_MP4DecConfigDescr(self, path, data):
|
||||
if self.reject(path, len(data), 13, False): return
|
||||
otid, flags, buf_hi, buf_lo, rate_max, rate_avg = \
|
||||
struct.unpack(">BBBHLL", data[:13])
|
||||
self.settrack('bitrate', int(rate_avg / 1000))
|
||||
objtype = MP4ObjectTypeMap.get(otid, None)
|
||||
if not objtype: return # some unknown format
|
||||
self.parse_mp4desc(path + [objtype], data[13:])
|
||||
|
||||
def parse_MP4DecSpecificDescr(self, path, data):
|
||||
try:
|
||||
parser = getattr(self, "parse_MP4DecSpecificDescr_" + path[-2])
|
||||
except AttributeError:
|
||||
return # unknown format
|
||||
except IndexError:
|
||||
return self.err("internal error")
|
||||
parser(path, data)
|
||||
|
||||
def parse_MP4DecSpecificDescr_MPEG4Audio(self, path, data):
|
||||
if self.reject(path, len(data), 2, False): return
|
||||
a, data = chop(data)
|
||||
b, data = chop(data)
|
||||
profile = (a >> 3) & 0x1F
|
||||
freq = ((a << 1) | (b >> 7)) & 0x0F
|
||||
try:
|
||||
self.settrack('filetype', "MPEG-4 " + MP4ProfileMap[profile])
|
||||
except KeyError:
|
||||
pass
|
||||
if freq == 15:
|
||||
if self.reject(path, len(data), 3, False): return
|
||||
freq = b & 0x7F
|
||||
b, data = chop(data)
|
||||
freq = (freq << 7) | b
|
||||
b, data = chop(data)
|
||||
freq = (freq << 7) | b
|
||||
b, data = chop(data)
|
||||
freq = (freq << 1) | (b >> 7)
|
||||
else:
|
||||
try:
|
||||
freq = (96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350)[freq]
|
||||
except IndexError:
|
||||
self.err(path, "invalid sampling rate code %d" % freq)
|
||||
freq = 0
|
||||
if freq:
|
||||
ref = self.gettrack('sample rate')
|
||||
if not ref:
|
||||
self.settrack('sample rate', freq)
|
||||
elif freq != ref:
|
||||
self.err(path, "sample rate in AAC descriptor (%d) doesn't match sample rate in stream description (%d)" % (freq, ref))
|
||||
if data:
|
||||
return self.err(path, "descriptor is longer than expected")
|
||||
|
||||
def parse_MP4DecSpecificDescr_MPEG4Visual(self, path, data):
|
||||
self.settrack('video format', 'MPEG-4 ASP')
|
||||
|
||||
def parse_meta(self, start, size, path):
|
||||
self.parse_container(start + 4, size - 4, path)
|
||||
|
||||
def parse_data(self, start, size, path):
|
||||
if self.reject(path, size, 8, False): return
|
||||
try:
|
||||
parser, key = QTMetaDataMap[path[-2]]
|
||||
except IndexError:
|
||||
return self.err(path, "data atom found at root level")
|
||||
except KeyError:
|
||||
return self.err(path, "no parser defined for this atom")
|
||||
format = struct.unpack(">L", self.f.read(8)[:4])[0] & 0x00FFFFFF
|
||||
try:
|
||||
parser = getattr(self, "format_" + parser)
|
||||
except AttributeError:
|
||||
return self.err(path, "format parser `%s' doesn't exist" % parser)
|
||||
res = parser(path, self.f.read(size - 8))
|
||||
if key:
|
||||
if res is None:
|
||||
return self.err(path, "decoding failed, no value assigned")
|
||||
self.info[key] = res
|
||||
|
||||
def format_text(self, path, data):
|
||||
data = data.strip("\0")
|
||||
if data.startswith("\xfeff"):
|
||||
return unicode(data, 'utf_16')
|
||||
else:
|
||||
return unicode(data, 'utf_8')
|
||||
|
||||
def format_year(self, path, data):
|
||||
data = data.strip("\0").split('-', 1)[0]
|
||||
try:
|
||||
return int(data)
|
||||
except ValueError:
|
||||
return self.err(path, "invalid date format")
|
||||
|
||||
def format_byte(self, path, data):
|
||||
if not data:
|
||||
return self.err(path, "zero-length data block")
|
||||
return ord(data[0])
|
||||
|
||||
def format_genre(self, path, data):
|
||||
if not data:
|
||||
return self.err(path, "zero-length data block")
|
||||
genre = ID3v1Genres.get(ord(data[-1]) - 1, None)
|
||||
if genre: self.info["genre"] = genre
|
||||
|
||||
def format_track(self, path, data, item='track'):
|
||||
if self.reject(path, len(data), 6, False): return
|
||||
current, total = struct.unpack(">HH", data[2:6])
|
||||
if current: self.info["%s number" % item] = current
|
||||
if total: self.info["total %ss" % item] = total
|
||||
|
||||
def format_disc(self, path, data):
|
||||
return self.format_track(path, data, 'disc')
|
||||
|
||||
def format_artwork(self, path, data):
|
||||
self.artwork.append(data)
|
||||
|
||||
def format_flag(self, path, data):
|
||||
return len(data.strip("\0")) != 0
|
||||
|
||||
def get_repear_info(self):
|
||||
info = {}
|
||||
have_video = False
|
||||
have_audio = False
|
||||
for track in self.tracks.itervalues():
|
||||
ttype = track.get('type', '?')
|
||||
if not(have_audio) and (ttype == 'audio'):
|
||||
ainfo = track.copy()
|
||||
dictremove(ainfo, ('type', 'width', 'height'))
|
||||
info.update(ainfo)
|
||||
have_audio = True
|
||||
if not(have_video) and (ttype == 'video'):
|
||||
vinfo = track.copy()
|
||||
dictremove(vinfo, ('type',))
|
||||
if have_audio: dictremove(vinfo, ('format', ))
|
||||
info.update(vinfo)
|
||||
have_video = True
|
||||
info.update(self.info)
|
||||
if ('album artist' in info) and not('artist' in info):
|
||||
info['artist'] = info['album artist']
|
||||
if have_video:
|
||||
info['filetype'] = "MPEG-4 Video file"
|
||||
elif not('filetype' in info):
|
||||
info['filetype'] = "MPEG-4 Audio File"
|
||||
return info
|
||||
|
||||
################################################################################
|
||||
|
||||
def dump_dict(d):
|
||||
keys = d.keys()
|
||||
keys.sort()
|
||||
for key in keys:
|
||||
print " %s = %s" % (key, repr(d[key]))
|
||||
|
||||
if __name__ == "__main__":
|
||||
qt = QTParser(file(sys.argv[1], "rb"), True)
|
||||
print
|
||||
|
||||
print "Raw file information:"
|
||||
dump_dict(qt.info)
|
||||
for track in qt.tracks:
|
||||
print "Raw track information (id %s):" % track
|
||||
dump_dict(qt.tracks[track])
|
||||
print
|
||||
|
||||
print "rePear-compliant information:"
|
||||
dump_dict(qt.get_repear_info())
|
||||
print
|
||||
|
||||
if qt.errors:
|
||||
print "Errors:"
|
||||
for e in qt.errors: print " %s: %s" % e
|
||||
print
|
||||
32
repear_playlists.ini
Normal file
32
repear_playlists.ini
Normal file
@@ -0,0 +1,32 @@
|
||||
; ***** rePear master playlist file *****
|
||||
; This file contains descriptions of playlists that rePear shall create
|
||||
; automatically based on certain properties of the audio files. It consists of
|
||||
; two parts: global options and playlist specifications.
|
||||
; For more detailed information on how to use this file, read usage.html
|
||||
|
||||
; ----- global options -----
|
||||
|
||||
skip album playlists = yes
|
||||
; YES if .m3u files that are just a directory index shall be skipped [default]
|
||||
; NO if every .m3u playlist found on the iPod shall be turned into a playlist
|
||||
|
||||
directory playlists = no
|
||||
; YES if rePear shall create a playlist for *every* folder found on the iPod
|
||||
; NO if it shall not do that [default]
|
||||
|
||||
; ----- playlist specifications -----
|
||||
; playlist specifications use the following syntax:
|
||||
; [Playlist Name] ; the playlist name
|
||||
; include = /path/to/directory ; include a directory into this playlist
|
||||
; exclude = /another/diretory ; exclude a directory from the playlist
|
||||
; new = 1 ; include all new tracks [optional]
|
||||
; changed = 1 ; include all changed tracks [optional]
|
||||
; shuffle = balanced ; enable balanced playlist shuffling [opt.]
|
||||
; shuffle = random ; enable random playlist shuffling [opt.]
|
||||
; sort = <criteria> ; sort tracks [optional, see documentation]
|
||||
|
||||
; an example playlist that contains all tracks that have been modified since
|
||||
; the last freeze operation
|
||||
[Hot New Stuff]
|
||||
new = 1
|
||||
changed = 1
|
||||
12
repear_scrobble.ini
Normal file
12
repear_scrobble.ini
Normal file
@@ -0,0 +1,12 @@
|
||||
; ***** rePear last.fm scrobbling configuration file *****
|
||||
; This file contains your login credentials for the last.fm service and
|
||||
; (optionally) file or directory patterns to exclude from scrobbling.
|
||||
|
||||
username =
|
||||
password =
|
||||
; Note: instead of the password itself, you can also specify the MD5 hash
|
||||
; of the password
|
||||
|
||||
; example excludes:
|
||||
; exclude = /some/directory
|
||||
; exclude = *.podcast.mp3
|
||||
243
scrobble.py
Normal file
243
scrobble.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# last.fm scrobbling library for rePear, the iPod database management tool
|
||||
# Copyright (C) 2008 Martin J. Fiedler <martin.fiedler@gmx.net>
|
||||
#
|
||||
# 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
|
||||
|
||||
import sys, urllib, urllib2, re, time, md5, types, fnmatch, os
|
||||
|
||||
try:
|
||||
import repear
|
||||
client_id = "rpr"
|
||||
client_ver = repear.__version__
|
||||
except (ImportError, NameError, AttributeError):
|
||||
client_id = "tst"
|
||||
client_ver = "1.0"
|
||||
server = "http://post.audioscrobbler.com:80/"
|
||||
protocol_version = "1.2"
|
||||
max_queue = 50
|
||||
|
||||
|
||||
def utf8urlencode(x):
|
||||
if type(x) != types.UnicodeType:
|
||||
x = unicode(x, sys.getfilesystemencoding(), 'replace')
|
||||
return urllib.quote(x.encode('utf-8', 'replace'))
|
||||
|
||||
|
||||
class ScrobbleError(Exception): pass
|
||||
|
||||
|
||||
class Scrobbler:
|
||||
def __init__(self, user=None, password=None):
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.excludes = []
|
||||
self.queue = []
|
||||
self.index = {}
|
||||
|
||||
def _add(self, item):
|
||||
key = "&".join(map(str, item[:3])).lower()
|
||||
if not(key in self.index):
|
||||
self.queue.append(item)
|
||||
self.index[key] = True
|
||||
|
||||
def config(self, filename):
|
||||
try:
|
||||
f = file(filename, "r")
|
||||
for line in f:
|
||||
line = line.split(';', 1)[0]
|
||||
if not(line) or not('=' in line):
|
||||
continue
|
||||
key, value = [x.strip() for x in line.split('=', 1)]
|
||||
key = key.lower()
|
||||
if key.startswith("user"):
|
||||
self.user = value
|
||||
elif key.startswith("pass"):
|
||||
self.password = value
|
||||
elif key.startswith("exclude"):
|
||||
if value[0] == "/":
|
||||
value = value[1:]
|
||||
if os.path.isdir(value):
|
||||
if value[-1] != "/":
|
||||
value += "/"
|
||||
value += "*"
|
||||
self.excludes.append(value.lower())
|
||||
f.close()
|
||||
except IOError:
|
||||
return False
|
||||
return not(not(self.user)) and not(not(self.password))
|
||||
|
||||
def load(self, filename):
|
||||
try:
|
||||
f = file(filename, "r")
|
||||
for line in f:
|
||||
line = line.strip().split('&')
|
||||
try:
|
||||
line[0] = long(line[0])
|
||||
except ValueError:
|
||||
line = []
|
||||
if len(line) == 6:
|
||||
self._add(tuple(line))
|
||||
f.close()
|
||||
except IOError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def save(self, filename):
|
||||
data = "\n".join(["&".join(map(str, item)) for item in self.queue]) + "\n"
|
||||
try:
|
||||
f = file(filename, "w")
|
||||
f.write(data)
|
||||
f.close()
|
||||
except IOError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __iadd__(self, item):
|
||||
self.enqueue(item)
|
||||
return self
|
||||
def enqueue(self, item):
|
||||
path = item.get('original path', None) or item.get('path', None)
|
||||
if self.excludes and path:
|
||||
if type(path) == types.UnicodeType:
|
||||
path = path.encode(sys.getfilesystemencoding(), 'replace')
|
||||
path = path.lower()
|
||||
for pattern in self.excludes:
|
||||
if fnmatch.fnmatch(path, pattern):
|
||||
return
|
||||
try:
|
||||
artist = utf8urlencode(item['artist'])
|
||||
title = utf8urlencode(item['title'])
|
||||
length = str(long(item['length']))
|
||||
playtime = long(item['last played time'])
|
||||
except KeyError:
|
||||
return
|
||||
album = utf8urlencode(item.get('album', ''))
|
||||
track = str(item.get('track number', ''))
|
||||
self._add((playtime, artist, title, length, album, track))
|
||||
|
||||
def scrobble(self):
|
||||
if not(self.user) or not(self.password):
|
||||
raise ScrobbleError, "user name or password missing"
|
||||
if not(self.queue):
|
||||
return
|
||||
|
||||
# build authentication request
|
||||
if re.match(r'^[0-9a-fA-F]{32}$', self.password):
|
||||
self.password = self.password.lower()
|
||||
else:
|
||||
self.password = md5.md5(self.password).hexdigest()
|
||||
timestamp = str(long(time.time()))
|
||||
auth = md5.md5(self.password + timestamp).hexdigest()
|
||||
req = urllib2.Request(
|
||||
"%s?hs=true&p=%s&c=%s&v=%s&u=%s&t=%s&a=%s" % \
|
||||
(server, protocol_version, client_id, client_ver, self.user, timestamp, auth))
|
||||
|
||||
# send and read authentication request
|
||||
try:
|
||||
res = urllib2.urlopen(req).read()
|
||||
# res = "OK\nfoobar\n\nhttp://localhost:1337/foo"
|
||||
except urllib2.HTTPError, e:
|
||||
raise ScrobbleError, "HTTP %d in authentication phase" % e.code
|
||||
except urllib2.URLError, e:
|
||||
raise ScrobbleError, "network error in authentication phase: %s" % e.reason.args[1]
|
||||
except IOError:
|
||||
raise ScrobbleError, "read error in authentication phase"
|
||||
res = [line.strip() for line in res.split("\n")]
|
||||
code = res[0].split()[0].upper()
|
||||
|
||||
# check authentication response
|
||||
if code == "BANNED":
|
||||
raise ScrobbleError, "client banned"
|
||||
elif code == "BADAUTH":
|
||||
raise ScrobbleError, "invalid username or password"
|
||||
elif code == "BADTIME":
|
||||
raise ScrobbleError, "system clock is skewed"
|
||||
elif code == "FAILED":
|
||||
raise ScrobbleError, res[0].split(" ", 1)[-1]
|
||||
elif code != "OK":
|
||||
raise ScrobbleError, "invalid answer from server: " + res[0]
|
||||
try:
|
||||
sid = res[1]
|
||||
url = res[3]
|
||||
except IndexError:
|
||||
raise ScrobbleError, "malformed authentication response"
|
||||
if not url.startswith("http://"):
|
||||
raise ScrobbleError, "malformed authentication response"
|
||||
|
||||
# submit queued items
|
||||
self.queue.sort()
|
||||
while self.queue:
|
||||
# build POST request string
|
||||
data = "s=" + sid
|
||||
for i in xrange(min(len(self.queue), 50)):
|
||||
playtime, artist, title, length, album, track = self.queue[i]
|
||||
data += "&a[%d]=%s&t[%d]=%s&i[%d]=%d&o[%d]=P&r[%d]=&l[%d]=%s&b[%d]=%s&n[%d]=%s&m[%d]=" % \
|
||||
(i, artist, i, title, i, playtime, i, i, i, length, i, album, i, track, i)
|
||||
# print data
|
||||
|
||||
# send and read submission request
|
||||
try:
|
||||
res = urllib2.urlopen(url, data).read()
|
||||
# res = "mist"
|
||||
except urllib2.HTTPError, e:
|
||||
raise ScrobbleError, "HTTP %d in submission phase" % e.code
|
||||
except urllib2.URLError, e:
|
||||
raise ScrobbleError, "network error in submission phase: %s" % e.reason.args[1]
|
||||
except IOError:
|
||||
raise ScrobbleError, "read error in submission phase"
|
||||
res = res.strip().split("\n", 1)[0].strip()
|
||||
code = res.split()[0].upper()
|
||||
|
||||
# check response
|
||||
if code == "BADSESSION":
|
||||
raise ScrobbleError, "invalid session while submitting"
|
||||
elif code == "FAILED":
|
||||
raise ScrobbleError, res.split(" ", 1)[-1]
|
||||
elif code != "OK":
|
||||
raise ScrobbleError, "invalid answer from server: " + res
|
||||
|
||||
# finally, remove items from the queue
|
||||
del self.queue[:max_queue]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
s = Scrobbler()
|
||||
s.load()
|
||||
print "old queue:", len(s.queue), "items"
|
||||
s += {
|
||||
'last played time': 1205685904L,
|
||||
'album': u'Story of Ohm',
|
||||
'artist': u'paniq',
|
||||
'length': 310.43918367346942,
|
||||
'title': u'Liberation',
|
||||
'track number': 4,
|
||||
}
|
||||
s += {
|
||||
'last played time': 1205686220L,
|
||||
'album': u'Story of Ohm',
|
||||
'artist': u'paniq',
|
||||
'length': 228.04897959183674,
|
||||
'title': u'Story of Ohm',
|
||||
'track number': 6,
|
||||
}
|
||||
print "new queue:", len(s.queue), "items"
|
||||
try:
|
||||
s.scrobble()
|
||||
except ScrobbleError, e:
|
||||
print e
|
||||
print "after scrobbling:", len(s.queue), "items"
|
||||
s.save()
|
||||
349
usage.html
Normal file
349
usage.html
Normal file
@@ -0,0 +1,349 @@
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
|
||||
<title>rePear User's Guide</title>
|
||||
</head><body>
|
||||
<!--SOT-->
|
||||
|
||||
<h1>rePear User's Guide</h1>
|
||||
|
||||
<p id="version">
|
||||
<strong>Author:</strong> Martin J. Fiedler <<a href="mailto:martin.fiedler@gmx.net">martin.fiedler@gmx.net</a>><br />
|
||||
<strong>Version:</strong> 0.4.1<br />
|
||||
<strong>Date:</strong> 2009-02-25</p>
|
||||
|
||||
|
||||
<h2>For the impatient ...</h2>
|
||||
|
||||
<p><strong>Initial Setup</strong></p>
|
||||
<ol class="indented">
|
||||
<li>initialize the iPod with iTunes</li>
|
||||
<li>remove all tracks from the iPod with iTunes (optional)</li>
|
||||
<li>install rePear on the iPod<ul>
|
||||
<li>On Windows: unpack the .zip file into the iPod's root directory</li>
|
||||
<li>On other systems: copy rePear (the <code>.py</code> files) into the iPod's root directory</li></ul></li>
|
||||
<li>run <code>repear config</code> or use the GUI (<code>rePearUI.exe</code>) to configure rePear</li>
|
||||
<li>run <code>repear dissect</code> or click the »dissect« button in the GUI to have the existing iTunes music library broken down into files and folders with an <code>/artist/album/title</code> scheme (optional, <span style="color:red;">dangerous</span>)</li>
|
||||
<li>copy music onto the iPod with the file manager you like best</li>
|
||||
<li>run rePear to <strong>freeze</strong> the database</li>
|
||||
<li>wait</li>
|
||||
<li>safely(!) disconnect the iPod</li>
|
||||
<li>listen to your music (optional, but recommended :)</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Regular Maintenance</strong> (i.e. Manage your Music)</p>
|
||||
<ol class="indented">
|
||||
<li>connect the iPod to your computer</li>
|
||||
<li>run rePear to <strong>unfreeze</strong> the database</li>
|
||||
<li>do whatever you want with your music files</li>
|
||||
<li>run rePear to <strong>freeze</strong> the database again</li>
|
||||
<li>safely(!) disconnect the iPod</li>
|
||||
<li>enjoy your music</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<h2 class="bar">rePear Setup – Step by Step</h2>
|
||||
<p>First, hold your breath. This section is awfully long, but that's only because it's written in a very detailed manner. It should not take you longer than about half an hour to get everything going (or ten minutes if you already have a decent Python installation or are working on Windows).</p>
|
||||
|
||||
<p>Now, this is what you have to do to use rePear with your iPod:</p>
|
||||
|
||||
<h3>1. Prepare the iPod</h3>
|
||||
|
||||
<p>If your iPod is new, you have to connect it to your computer and have it initialized by iTunes at least one time. If you used your iPod with iTunes before, then it's up to you: You may delete all tracks from it to start all over with rePear, but you may as well keep the tracks installed by iTunes. If they contain correct ID3 tags and are all unencrypted MP3 or AAC, this should work fine.</p>
|
||||
|
||||
<p>You also have to find out where the iPod's root directory is. On Windows, this is the root directory of the iPod's drive letter (e.g. <code>L:\</code>). On Mac OS X, it's something like <code>/Volumes/My iPod</code>. On other Unixes, it depends – on my Linux box, it is usually be mounted at <code>/media/sda2</code>, for example.</p>
|
||||
|
||||
<p>Once you figured out where you can find the root directory, you should make a backup of the directory <code>/iPod_Control/iTunes</code>. rePear will automatically create a backup of the most important file in there, <code>iTunesDB</code>, but it is always better to have a second safety net. On Windows, it may be necessary to configure Explorer so that it shows hidden files and directories.</p>
|
||||
|
||||
|
||||
<h3>2. Install rePear</h3>
|
||||
|
||||
<p>If you are on a Windows box, <a href="index.php?page=download">download</a> the <code>.zip</code> file version of rePear and extract it into the iPod's root directory. You should get a <code>rePear</code> subdirectory and some files in the root directory. The two most importand of these are <code>repear.bat</code>, which is a small launcher script if you want to run the command-line version of rePear, and <code>rePearUI.exe</code>, the graphical user interface. This is all you need, everything else is already included in the ZIP file.</p>
|
||||
|
||||
<p>On UNIX-like operating systems, <a href="index.php?page=download">download</a> the <code>.tar.gz</code> version and put all the <code>.py</code> and <code>.ini</code> files from the downloaded archive into the iPod root directory (this is a recommendation; if you don't put rePear there, you will have to use the <code>–r</code> switch described below).<br />
|
||||
You will also need a <a href="http://www.python.org/">Python</a> runtime to use rePear. On most Unix distributions (including OS X), it is installed by default. If it isn't, use the package manager to install it or look at the <a href="http://www.python.org/download/">official download page</a> to see what to do. Every version of Python between 2.3 and 2.6 should work.<br />
|
||||
If you intend to make use of rePear's Ogg Vorbis transcoding feature, you also need to install OggDec (from the <a href="http://www.xiph.org/downloads/">vorbis-tools</a> package) and <a href="http://lame.sourceforge.net/">LAME</a>. On Unix-like systems, a vorbis-tools package should be available from your package manager, but you may have to compile and install LAME for yourself.<br />
|
||||
Furthermore, the cover artwork features require the <a href="http://www.pythonware.com/products/pil/">Python Imaging Library</a> (PIL). Again, this should be available through the package manager.</p>
|
||||
|
||||
<p>The »direct« way to start the command-line version rePear is to open a console window (on Windows: Start Menu, Run, <code>cmd</code>), go to the iPod root directory (e.g. with <code>L:</code> or <code>cd "/Volumes/My iPod"</code>) and run <code>repear </code><i>[options here]</i> (Windows) or <code>python repear.py </code><i>[options here]</i> (Unix). This method is needed if you want to specify options or tell rePear which action to perform. If you don't need to do this (and you won't for most of the time), you can as well double-click <code>repear.bat</code> on Windows or use the graphical user interface, which has buttons for the most commonly used operations. On Mac OS X, after double-clicking <code>repear.py</code>, the system will ask whether to execute the program in the Terminal when you double-click it the first time. Confirm this message box. If it tells you that »no default application is specified«, click »Choose Application« and select »Terminal«.</p>
|
||||
|
||||
|
||||
<h3>3. Configure rePear</h3>
|
||||
|
||||
<p>Some of rePear's features require a moderate amount of configuration – namely, the exact iPod model must be known to generate cover artwork and to use the last.fm scrobbling interface, you need to specify your last.fm login data.</p>
|
||||
|
||||
<p>These configuration steps can be done on the command line by running <code>repear config</code>. This will start a small text-based wizard that asks you to pick your model from a list and specify your last.fm credentials. If you don't use last.fm or don't want to use that feature, just press ENTER when asked for the login name.</p>
|
||||
|
||||
<p>In the Windows GUI, there's a »config« button that opens a small dialog window with the same options: a drop-down menu for the model selection and two fields for the last.fm login data. Just make the appropriate settings here and click »OK«.</p>
|
||||
|
||||
<p>Another very important step to follow is synchronizing the clocks of the iPod and the computer: If you intend to use last.fm scrobbling, a precise record of when which track has beed played is essential. So please double-check that the iPod's and the computer's clock agree and are set to the same timezome. <strong>This is important</strong>, because last.fm might reject your scrobbled tracks if there are any inconsistencies in the play times.</p>
|
||||
|
||||
|
||||
<h3>4. Import tracks (optional)</h3>
|
||||
|
||||
<p>If you still have some tracks in the iPod database that were previously installed with iTunes, you can either keep them in place or have rePear sort them back into directories. If your iPod is empty, just skip this step completely.</p>
|
||||
|
||||
<p>If you choose to keep them inside the iPod's private directory structures, it will be fine. But note that you will not be able to modify them any further (not even delete them) without iTunes.</p>
|
||||
|
||||
<p>The other option is to have rePear <strong>dissect</strong> the iPod's music database. To do this, start rePear with the <code>dissect</code> parameter (like in <code>repear dissect</code>) or click the »dissect« button in the GUI. After asking you to confirm your command, rePear will create a new folder <code>Dissected Tracks</code> on the iPod. Inside this folder, rePear will create a typical artist/album/track directory structure. For example, you will get files like <code>/Dissected Tracks/Some Artist/Nice Album/07 – Some Track.mp3</code>. Tracks with album information missing will be put directly in the artist's folder. Tracks without even an artist name will be put directly into <code>Dissected Tracks</code>.</p>
|
||||
|
||||
|
||||
<h3>5. Copy your tracks onto the iPod</h3>
|
||||
|
||||
<p>Now it's time to make the USB port glow! You can now copy, delete, move or rename tracks on your iPod. You can create virtually any directory structure you want. It's up to you how to organize the files. In particular, you don't need to put them in any special location. iTunes stores them with cryptic names in <code>/iPod_Control/Music</code>, but you don't need to do that. In fact, it's stronly suggested not to put your self-managed files here – just create a folder elsewhere on the iPod. For example, all my music files go directly into <code>/Music</code>.</p>
|
||||
|
||||
|
||||
<h3>6. Run rePear</h3>
|
||||
|
||||
<p>Now it's time to run rePear without parameters or select the highlighted »freeze« option in the GUI. It will do what I refer to as <strong>freezing</strong> the iPod's database: It will scan the whole filesystem of the iPod for MP3, Ogg and AAC files and move them into the hidden area where the playable tracks are kept, transcoding Ogg to MP3 as needed. While doing this, rePear analyzes each file to get the ID3 metadata information to finally generate a new music database on the iPod.</p>
|
||||
|
||||
<p>I'm afraid that this process is quite a lengthy one – rePear has a »metadata cache« that remembers all information about the tracks so that they don't need to be re-scanned the next time when new files are added, but moving the files is very time-consuming in itself. You should make sure to enable the operating system's write cache to get best performance. As a rule of thumb, freezing a 8 GB flash-based iPod should take 2 to 5 minutes.</p>
|
||||
|
||||
|
||||
<h3>7. Disconnect the iPod and listen to your music</h3>
|
||||
|
||||
<p>After the freezing process is complete, you can disconnect the iPod. You should always do this in a safe manner (»safe removal of devices« in the Taskbar Notification Area on Windows; the eject icon next to the iPod volume's icon in Finder on OS X; <code>umount</code> on other Unixes).</p>
|
||||
|
||||
<p>You should now be able to listen to your music. If not, please <a href="mailto:martin.fiedler@gmx.net?subject=[rePear]%20">send me a bug report</a> with a precise description of what exactly went wrong, including the log file generated by rePear and a (compressed!) copy of the <code>/iPod_Control/iTunes/iTunesDB</code> file in both versions: The one created by rePear and the one from your previous backup. (And now guess what that backup was good for ... not only for sending me bug reports, but also for fixing your broken iPod again!)</p>
|
||||
|
||||
|
||||
|
||||
<h2 class="bar">Managing the rePear tracks – Step by Step</h2>
|
||||
|
||||
<p>Let's imagine that everything went OK (and I hope it did!) and now you want to put new music onto the iPod, remove old tracks or do other maintaining tasks. Here is what you have to do:</p>
|
||||
|
||||
|
||||
<h3>1. Plug in the iPod and run rePear</h3>
|
||||
|
||||
<p>If you start rePear without any parameters, it will remember that the databaze is frozen and automatically do the right thing: <strong>unfreeze</strong> it. The GUI doesn't do this completely automatically, but at least it will recognize that »unfreeze« is the most logical thing to do and thus, it will highlight the according button.</p>
|
||||
|
||||
<p>Unfreezing means that all the files you put on the iPod that mysteriously disappeared while freezing (because they were moved to the iPod's sected music hideout) will be put back again. This will not take as long as freezing it, but on iPods that are slow with I/O (most newer models), it will be close.</p>
|
||||
|
||||
|
||||
<h3>2. Manage your Music</h3>
|
||||
|
||||
<p>If there were no errors, all the files will be back at their original locations and you can now start to copy, move and rename files or directories at will.</p>
|
||||
|
||||
|
||||
<h3>3. Run rePear again</h3>
|
||||
|
||||
<p>Before disconnecting the iPod, you have to run rePear once again so that it can <strong>freeze</strong> the iPod's music database and move the music files away again. All files that you left untouched will be processed in an instant; new or modified files will take a little bit longer to process.</p>
|
||||
|
||||
<p>If you enabled the last.fm scrobbling feature, this will also be the point where the information about when you listened to which tracks will be submitted to last.fm. If a track has been played more than once since the last <code>freeze</code> or <code>update</code>, only the last time it has been played will be submitted. This isn't rePear's fault – the iPod simply only saves the last playback time.<br />
|
||||
If any error occurs while uploading the information to last.fm (like a broken network connection, or just because you're offline), the information will not be lost. Instead, it will be stored and re-submitted the next time the <code>freeze</code> or <code>update</code> action is executed.</p>
|
||||
|
||||
|
||||
<h3>4. Disconnect the iPod and listen to your music</h3>
|
||||
|
||||
<p>Just like in the setup procedure described above, you may now safely (I mean it!) disconnect the iPod and enjoy your music.</p>
|
||||
|
||||
|
||||
|
||||
<h2 class="bar">Details on playlists</h2>
|
||||
|
||||
<p>There are two ways to generate playlists with rePear: You can put normal M3U playlist files anywhere on the iPod and rePear will add them to the Playlist menu. These playlists will have the same name as the <code>.m3u</code> file they're created from. The directory name won't be included, so please make sure you don't have a dozen <code>.m3u</code> files with same names on your iPod.</p>
|
||||
|
||||
<p>The other, more advanced method of playlist generation are »automatic« playlists that are created from the main database with some user-specified rules. In the default configuration, there's already one such playlist: »Hot New Stuff« is where rePear puts all new and changed tracks it found into. If you don't like that, you can disable that, of course. This chapter describes how to do that.</p>
|
||||
|
||||
<h3>The master playlist file</h3>
|
||||
|
||||
<p>All automatic playlists and other playlist options are specified in a single file, which is <code>repear_playlists.ini</code> in the root directory of the iPod. If this file is not present, rePear assumes default options and doesn't generate any automatic playlists (not even »Hot New Stuff«).</p>
|
||||
|
||||
<p>The master playlist file looks like a normal Windows INI file, which means it is a text file with lines in the »<code>key = value</code>« format. It is subdivided into sections that start with a line with the section name in square brackets, like »<code>[this]</code>«. Comments start with a semicolon (<code>;</code>).</p>
|
||||
|
||||
<p>Each section in the master playlist file describes one playlist. The playlist name is derived from the section title. There's one exception, though: The first part of the file, before the first section header, is used for general, global playlist options.</p>
|
||||
|
||||
<p>Some options require boolean values. In this case, you can use either <code>1</code>/<code>0</code>, <code>yes</code>/<code>no</code>, <code>y</code>/<code>n</code>, <code>on</code>/<code>off</code>, <code>true</code>/<code>false</code> or <code>enable</code>/<code>disable</code> to specify whether that option shall be used or not.</p>
|
||||
|
||||
<h3>Global playlist options</h3>
|
||||
|
||||
<p>The following options are available in the global options (pseudo-)section at the beginning of the <code>repear_playlists.ini</code> file:</p>
|
||||
|
||||
<dl>
|
||||
|
||||
<dt><code>skip album playlists</code> <em>(boolean, default: enabled)</em></dt>
|
||||
<dd>This option specifies whether playlists that cover exactly one album will be included or not. Normally, these playlists are pointless: The album appears under »Albums«, there's no reason why it should be under »Playlists«, too. This means that you can keep the <code>.m3u</code> files that usually come with album downloads, without having them clutter your Playlists menu on the iPod. That's why this option is enabled by default – if you don't like it, you can disable it, though.</dd>
|
||||
|
||||
<dt><code>directory playlists</code> <em>(boolean, default: disabled)</em></dt>
|
||||
<dd>If this option is enabled, rePear will create a playlist for <strong>every</strong> folder on the iPod filesystem it finds playable files in. Note that the playlist name will only contain the last component of the path name. The files in <code>/Music/foo/bar/*.mp3</code> go into a playlist called »bar«, for example.</dd>
|
||||
|
||||
</dl>
|
||||
|
||||
<h3>Automatic playlist options</h3>
|
||||
|
||||
<p>The following options can be used to specify automatic playlists:</p>
|
||||
|
||||
<dl>
|
||||
|
||||
<dt><code>include = </code><em><path></em></dt>
|
||||
<dd>Specifies which files shall be included in the playlist, either using a filename pattern like »<code>*.mp3</code>« or a directory name like »<code>/Music</code>«. In the latter case, the specified directory and all subdirectories will be included.<br />
|
||||
By default, no files or directories are included in a playlist. This means that a playlist without any <code>include</code> (or <code>new</code> or <code>changed</code>) statements will be empty. So, to make a usable playlists, there needs to be at least one <code>include</code> statement. The number of <code>include</code>s per playlists is unlimited, so it's perfectly possible to include multiple directories.</dd>
|
||||
|
||||
<dt><code>exclude = </code><em><path></em></dt>
|
||||
<dd>This works like <code>include</code>, but it specifies which files shall <strong>not</strong> be included in the playlist. <code>exclude</code> is »stronger« than <code>include</code>, so you can exclude subdirectories of other directories. The number of <code>exclude</code>s per playlists is also unlimited.</dd>
|
||||
|
||||
<dt><code>new = </code><em><boolean></em></dt>
|
||||
<dd>If this option is enabled, the playlist will contain <strong>all</strong> files that reTune found that were not present during the last <code>freeze</code> operation. Note that <code>new</code> is even stronger than <code>include</code> or <code>exclude</code>.</dd>
|
||||
|
||||
<dt><code>changed = </code><em><boolean></em></dt>
|
||||
<dd>If this option is enabled, the playlist will contain <strong>all</strong> files whose metadata changed since the last <code>freeze</code> operation. Like <code>new</code>, <code>changed</code> is even »stronger« than <code>include</code> or <code>exclude</code>.</dd>
|
||||
|
||||
<dt><code>shuffle = </code><em><mode></em></dt>
|
||||
<dd>Selects whether or not the playlist shall be shuffled, and which shuffle algorithm shall be used. The default (<code>0</code>, <code>no</code>, <code>off</code>, <code>false</code>, <code>disabled</code> or <code>none</code>) will keep the tracks in their original order. When this option is enabled (using <code>1</code>, <code>yes</code>, <code>on</code>, <code>true</code>, <code>enabled</code> or <code>balanced</code>), an advanced shuffle algorithm will be used that creates a not completely random, but very homogenous order of the tracks. The detailed algorithm is described on <a href="http://keyj.s2000.ws/?p=66">this web page</a>. Alternatively, a normal random shuffle can be selected with <code>2</code>, <code>random</code> or <code>standard</code>.</dd>
|
||||
|
||||
<dt><code>sort = </code><em><criteria></em></dt>
|
||||
<dd>This option specifies the criteria after which the tracks in the playlist shall be sorted. For a detailed explanation of the syntax of these criteria, read below. If there is neither a <code>sort</code> nor a <code>shuffle</code> statement in a playlist definition, the files will be sorted by path and filename. If sorting is enabled, it will take place <strong>after</strong> shuffling. This means that sorting is »stronger« than shuffling. If there are multiple <code>sort</code> options in a playlist definition, the sort operations will be performed in the same order as defined, so the last <code>sort</code> will be the strongest one.</dd>
|
||||
|
||||
</dl>
|
||||
|
||||
<h3>Sort criteria</h3>
|
||||
|
||||
<p>The following sort criteria are defined:</p>
|
||||
<table id="sortcriteria">
|
||||
<tr><th><code>title</code></th><td>Track title.</td></tr>
|
||||
<tr><th><code>artist</code></th><td>Track artist.</td></tr>
|
||||
<tr><th><code>album</code></th><td>Album title.</td></tr>
|
||||
<tr><th><code>year</code></th><td>Year the track has been published in.</td></tr>
|
||||
<tr><th><code>compilation</code></th><td>Whether the track is part of a compilation or not.</td></tr>
|
||||
<tr><th><code>rating</code></th><td>Rating, as set up in iTunes or the iPod itself.</td></tr>
|
||||
<tr><th><code>path</code></th><td>Path of the file.</td></tr>
|
||||
<tr><th><code>length</code></th><td>Length of the track.</td></tr>
|
||||
<tr><th><code>file size</code></th><td>Size of the file.</td></tr>
|
||||
<tr><th><code>mtime</code></th><td>Time of the last modification on the file.</td></tr>
|
||||
<tr><th><code>bitrate</code></th><td>Track bitrate.</td></tr>
|
||||
<tr><th><code>sample rate</code></th><td>Track sample rate.</td></tr>
|
||||
<tr><th><code>track number</code></th><td>The number of the track on the disc.</td></tr>
|
||||
<tr><th><code>disc number</code></th><td>The number of the disc in the set.</td></tr>
|
||||
<tr><th><code>total discs</code></th><td>The total number of discs in the set.</td></tr>
|
||||
<tr><th><code>artwork count</code></th><td>The number of cover artworks associated with the track.</td></tr>
|
||||
<tr><th><code>BPM</code></th><td>The BPM speed of the track.</td></tr>
|
||||
<tr><th><code>movie flag</code></th><td>Whether the track is a movie or not.</td></tr>
|
||||
<tr><th><code>play count</code></th><td>The number of times the track has been played completely.</td></tr>
|
||||
<tr><th><code>skip count</code></th><td>The number of times the track has been skipped over. Note that not every iPod stores this value.</td></tr>
|
||||
<tr><th><code>start count</code></th><td>The sum of <code>play count</code> and <code>skip count</code>, i.e. the number of times the track has been started.</td></tr>
|
||||
<tr><th><code>last played time</code></th><td>The time the track has last been played completely.</td></tr>
|
||||
<tr><th><code>last skipped time</code></th><td>The time the track has last been skipped over. Note that not every iPod stores this value.</td></tr>
|
||||
<tr><th><code>last started time</code></th><td>Either <code>last played time</code> or <code>last skipped time</code>, whichever is later. In other words, this is the last time the track has been started.</td></tr>
|
||||
</table>
|
||||
|
||||
<p>Multiple criteria can be combined with commas to form complex sort orders. For example »<code>artist, year, album, disc number, track number, title</code>« will sort the tracks by artist, the tracks of each artist will then be sorted by year, the tracks of each year will then be sorted by album, disc number, track number and finally by title. Note that sorting text values will always take place in a case-insensitive manner.</p>
|
||||
|
||||
<p>There are also various modifies for each subcriterion which are added as prefixes: A plus sign (»<code>+</code>«) will sort in ascending order, which is also the default. A minus sign (»<code>-</code>«) will sort in descending order. Angle brackets specify where tracks will be sorted that are missing the value associated with the sort criterion: With »<code><</code>«, empty values will be sorted to the front, with »<code>></code>« (the default), empty values will be sorted to the back of the list. The ordering of the empty values is always independent from the main sort order. »<code>-year</code>«, for example, will sort the tracks in descending order by time, but tracks that are missing the »year« metadata field will still appear at the end of the sorted list. If this is not desired, »<code><-year</code>« needs to be written.</p>
|
||||
|
||||
<h3>Playlist examples</h3>
|
||||
|
||||
<p>Finally, here's an example of how a <code>repear_playlists.ini</code> file could look like:</p>
|
||||
<pre>[Heavy Metal]
|
||||
include = /Music/Metal
|
||||
|
||||
[Random Soft Songs]
|
||||
include = /Music
|
||||
exclude = /Music/Metal
|
||||
exclude = /Music/Rock
|
||||
shuffle = 1
|
||||
sort = artist, album, title
|
||||
|
||||
[Audiobooks]
|
||||
include = *.book.mp3
|
||||
|
||||
[Hot New Stuff]
|
||||
new = 1
|
||||
changed = 1
|
||||
|
||||
[Randomized]
|
||||
include = /Music
|
||||
shuffle = 1
|
||||
sort = start count</pre>
|
||||
<p>Each playlist is specified by a header, containing the playlist name in square brackets, and a number of statements in a <code>key = value</code> form. In this example, we define three playlists: »Heavy Metal«, »Random Soft Songs« and »Audiobooks«.</p>
|
||||
|
||||
<p>The »Heavy Metal« playlist is a very simple one: It consists only of one <code>include</code> statement that tells rePear to put everything in the <code>/Music/Metal</code> directory (and its subdirectories) into that playlist. You could add more <code>include</code> statements if you like.</p>
|
||||
|
||||
<p>The »Random Soft Songs« playlist basically contains the whole <code>/Music</code> folder, ordered by Artist/Album/Title, but the two <code>exclude</code> statements remove the <code>Metal</code> and <code>Rock</code> subdirectories from that selection. So, what you get would be a collection of all your songs except Metal and Rock ones (given that you organize your music that way, of course). The <code>shuffle = 1</code> statement makes rePear shuffle all the songs in that playlist. If this statement is not present, rePear would add the songs in alphabetic order, subdirectories first – just like you see them in your file manager if »sort by filename« is selected.</p>
|
||||
|
||||
<p>The »Audiobooks« playlist makes use of a generic filename pattern instead of a directory name. It selects all files whose names end in <code>.book.mp3</code>, regardless of how scattered they are across the iPod's directory structure, and puts them into a common playlist.</p>
|
||||
|
||||
<p>The »Hot New Stuff» playlist is a special one: The <code>new</code> and <code>changed</code> statements act like special <code>include</code> statements, except that they don't match filenames, but the »freshness« of a file. If the <code>new</code> statement is active, rePear will include every file in the playlist that has not been there when the database was last frozen (note that this will include moved files as well!). Likewise, the <code>changed</code> statements instructs rePear to include every file that has been changed, but not added or renamed. In combination, these two statements build a playlist that mirrors every change since the last freeze operation.</p>
|
||||
|
||||
<p>Finally, there's the »Randomized» playlist, which simply contains the whole <code>/Music</code> folder in random order – or almost random order, because it's then sorted by start count. This has the effect that tracks that have been played less frequently occur early in the playlist, while tracks that are played often will be put at the end of the list.</p>
|
||||
|
||||
|
||||
<h2 class="bar">Details on artwork</h2>
|
||||
|
||||
<p>rePear's artwork support tries to make sensible assumptions on what image to display for every title. Consider, for example, a track like <code>/MyMusic/TheAlbum/TheTrack.mp3</code>. The image (JPEG or PNG format) shown for this track will be the first match from this list (sorted from highest to lowest priority):</p><ol>
|
||||
<li>an image file with the same name as the music file, but with the filename extension <code>.jpg</code> or <code>.png</code>, like <code>/MyMusic/TheAlbum/TheTrack.jpg</code></li>
|
||||
<li>an image file with the same name as the directory of the music file (except the extension, of course), like <code>/MyMusic/TheAlbum/TheAlbum.jpg</code></li>
|
||||
<li>an image file in the same directory as the music file that contains the word »front« somewhere in its name, like <code>/MyMusic/TheAlbum/front.jpg</code> or <code>/MyMusic/TheAlbum/Image_Front.jpg</code> (this rule is there to ensure that »front« covers are prioritzed over »back« covers, even though the latter ones come first in the alphabet)</li>
|
||||
<li>an image file in the same directory that contains the word »cover«, like <code>/MyMusic/TheAlbum/cover_image.jpg</code></li>
|
||||
<li>the first image file (in alphabetic order) in the directory, like <code>/MyMusic/TheAlbum/SomeImage.jpg</code></li>
|
||||
<li>an image file with the same name as the directory, but located in the parent directory, like <code>/MyMusic/TheAlbum.jpg</code></li>
|
||||
<li>the directoy-specific rules are inherited to subdirectories, i.e. if there is any directory-wide image file for one of the parent directories, it will be used for subdirectories, too; consider, for example, <code>/MyMusic/MyMusic.jpg</code></li>
|
||||
</ol>
|
||||
|
||||
<p>Please read rule #7 carefully: If you have some totally music-unrelated image files in the root directory, the first of these images will be assigned to every track of the iPod (except those that have higher-priority images, of course). You probably don't want that, so keep your folders clean :)</p>
|
||||
|
||||
|
||||
<h2 class="bar">Details on last.fm scrobbling</h2>
|
||||
|
||||
<p>Some options regarding the last.fm scrobble feature can be set up in another <code>.ini</code> file in the iPod's root directory: <code>repear_scrobble.ini</code> is located right next to <code>repear_playlists.ini</code> and is syntactically similar. It also looks like a Windows INI file, but it lacks the sections.</p>
|
||||
|
||||
<p>The most important things to set up in this file are <code>username</code> and <code>password</code> – these are your last.fm credentials. The password can be specified either in plaintext or as a MD5 sum. Neither method is really secure, but the MD5 sum at least makes it impossible to guess the plaintext password, so this is the recommended method. Normally, you don't need to set these values anyway, as the setup options in rePear and the GUI launcher already take care of this.</p>
|
||||
|
||||
<p>However, there are additional options in this file that need to be put there by hand if they're needed: The <code>exclude</code> options can specify directories or filename patterns for which scrobbling shall not take place. This is useful if you don't want some of your tracks appear in your last.fm profile, like audiobooks.</p>
|
||||
|
||||
|
||||
|
||||
<h2 class="bar">A more detailed look at rePear's options and actions</h2>
|
||||
|
||||
<p>As stated above, rePear automatically chooses between the <strong>freeze</strong> and <strong>unfreeze</strong> actions if it is started without parameters. However, you can override this by specifying the action as a command-line parameter (like in the <strong>dissect</strong> example: <code>repear dissect</code>). The following actions are available:</p>
|
||||
|
||||
<dl class="keywords">
|
||||
|
||||
<dt>help</dt><dd>Shows the brief help message and exits the program after that.</dd>
|
||||
|
||||
<dt>auto</dt><dd>Automatically choose between <strong>freeze</strong> and <strong>unfreeze</strong>, based on the current state of the cache. This is the default if no action is specified.</dd>
|
||||
|
||||
<dt>freeze</dt><dd>Scans the iPod for playable music files, moves them into the <code>/iPod_Control/Music</code> directory and generates an <code>iTunesDB</code> from it.</dd>
|
||||
|
||||
<dt>unfreeze</dt><dd>Moves music files that have previously been moved into <code>/iPod_Control/Music</code> by the freeze action back to their original locations.</dd>
|
||||
|
||||
<dt>update</dt><dd>Rebuilds <code>iTunesDB</code> based on rePear's internal cache with the data from the last freeze. In principle, this is identical to the freeze action, except that it doesn't search for new files. However, it will update play counts, scrobble tracks to last.fm and rebuild the automatic playlists specified in the <code>repear_playlists.ini</code> file.</dd>
|
||||
|
||||
<dt>dissect</dt><dd>Parses the current <code>iTunesDB</code> and moves all tracks found there into a directory following a <code>/Dissected Tracks/</code><artist><code>/</code><album><code>/</code><title> scheme.</dd>
|
||||
|
||||
<dt>reset</dt><dd>Deletes rePear's metadata cache. All information about the tracks installed on the iPod will be erased, but the music files themselves will remain. Note that if this is done while the database is in the frozen state, the information about the original filenames will be lost, too, so the files will be »trapped« in <code>/iPod_Control/Music</code>.</dd>
|
||||
|
||||
<dt>cfg-fwid</dt><dd>Tries to determine the serial number (FWID) of the currently attached iPod. This will also be done automatically during the first <code>freeze</code> operaton, but the auto-detection can also be started manually with this option.<br />
|
||||
The serial number needs to be known because some newer iPods (nano 3G, classic 6G and newer) require a checksum in the iTunesDB file that depends on the serial number.</dd>
|
||||
|
||||
<dt>cfg-model</dt><dd>Runs the (text-based) model selection wizard.</dd>
|
||||
|
||||
<dt>cfg-scrobble</dt><dd>Runs the (text-based) scrobble configuration wizard.</dd>
|
||||
|
||||
<dt>config</dt><dd>Runs all of the <code>cfg</code> options, one after another.</dd>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
<h3>Command-line options</h3>
|
||||
|
||||
<p>To get command-line help, run <code>repear –h</code>. This will also tell you about some other options that are available. These should be placed between <code>repear</code> and the action name:</p>
|
||||
|
||||
<ul>
|
||||
<li>Use <strong>–r</strong> <i>[some path]</i> to tell rePear where the iPod's root directory is. This is useful if rePear can't determine from what directory it was called, or if you deliberately don't want to keep rePear in the iPod's root directory.</li>
|
||||
<li><strong>–l</strong> <i>[some filename]</i> specifies where the rePear logfile should be written to.</li>
|
||||
<li><strong>–L</strong> <i>[options]</i> can be used to override the LAME encoding options that are used when transcoding Ogg files.</li>
|
||||
<li>Usage of the option <strong>–m</strong> <i>[model]</i> is required for the cover artwork feature. This option is only used for the <code>freeze</code> action, and its value is saved for upcoming freeze actions that won't need this option again. It can also be set using the configuration wizard or the GUI. Valid models are:
|
||||
<table id="modeltab">
|
||||
<tr><td><code>nano</code>, <code>nano1g</code> or <code>nano2g</code></td><td>iPod nano, first or second generation</td></tr>
|
||||
<tr><td><code>4g</code> or <code>photo</code></td><td>iPod photo (4th generation)</td></tr>
|
||||
<tr><td><code>5g</code> or <code>video</code></td><td>iPod video (5th generation)</td></tr>
|
||||
<tr><td><code>6g</code>, <code>classic</code> or <code>nano3g</code></td><td>iPod classic (6th generation) or iPod nano third generation (»fat nano«)</td><tr><td><code>nano4g</code></td><td>iPod nano 4th generation</td></tr>
|
||||
</table></li>
|
||||
<li><strong>–f</strong> deactivates the confirmation prompts that are shown when doing »uncommon« things.</li>
|
||||
<li><strong>–p</strong> <i>[some filename]</i> specifies the location of the master playlist file.</li>
|
||||
<li><strong>–s</strong> <i>[some filename]</i> specifies the location of the scrobble configuration file.</li>
|
||||
<li>On Windows systems, rePear will wait for a keypress after it is done. The <strong>––nowait</strong> option deactivates this behavior.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!--EOT-->
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user