diff options
author | Christophe Fergeau <teuf@gnome.org> | 2005-09-10 08:34:34 +0000 |
---|---|---|
committer | Christophe Fergeau <teuf@gnome.org> | 2005-09-10 08:34:34 +0000 |
commit | 27d9c7274b01f958315a9157a30f07359872d1aa (patch) | |
tree | 40a0bf871909c8a32c2fd12dd9e137dd960dc68b | |
parent | 91bac8f5d1c96f509d2abc7329d99729dc845589 (diff) | |
download | libgpod-tmz-27d9c7274b01f958315a9157a30f07359872d1aa.tar.gz libgpod-tmz-27d9c7274b01f958315a9157a30f07359872d1aa.tar.xz libgpod-tmz-27d9c7274b01f958315a9157a30f07359872d1aa.zip |
Initial import
git-svn-id: https://gtkpod.svn.sf.net/svnroot/gtkpod/libgpod/trunk@1080 f01d2545-417e-4e96-918e-98f8d0dbbcb6
-rw-r--r-- | AUTHORS | 2 | ||||
-rw-r--r-- | COPYING | 481 | ||||
-rw-r--r-- | ChangeLog | 19 | ||||
-rw-r--r-- | INSTALL | 229 | ||||
-rw-r--r-- | Makefile.am | 16 | ||||
-rw-r--r-- | NEWS | 0 | ||||
-rw-r--r-- | README | 0 | ||||
-rwxr-xr-x | autogen.sh | 20 | ||||
-rw-r--r-- | configure.ac | 120 | ||||
-rw-r--r-- | libgpod-1.0.pc.in | 11 | ||||
-rw-r--r-- | po/ChangeLog | 0 | ||||
-rw-r--r-- | po/Makefile.in.in | 263 | ||||
-rw-r--r-- | po/POTFILES.in | 2 | ||||
-rw-r--r-- | src/Makefile.am | 18 | ||||
-rw-r--r-- | src/itdb.h | 504 | ||||
-rw-r--r-- | src/itdb_itunesdb.c | 3622 | ||||
-rw-r--r-- | src/itdb_playlist.c | 1290 | ||||
-rw-r--r-- | src/itdb_private.h | 100 | ||||
-rw-r--r-- | src/itdb_track.c | 205 | ||||
-rw-r--r-- | tests/Makefile.am | 7 | ||||
-rw-r--r-- | tests/itdb_main.c | 88 |
21 files changed, 6997 insertions, 0 deletions
@@ -0,0 +1,2 @@ +Jorg Schuler <jcsjcs at users.sourceforge.net> (original gtkpod code) +Christophe Fergeau <teuf at gnome.org> (libraryfication) @@ -0,0 +1,481 @@ + GNU LIBRARY GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1991 Free Software Foundation, Inc. + 675 Mass Ave, Cambridge, MA 02139, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is + numbered 2 because it goes with version 2 of the ordinary GPL.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Library General Public License, applies to some +specially designated Free Software Foundation software, and to any +other libraries whose authors decide to use it. You can use it for +your libraries, 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 library, or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link a program with the library, you must provide +complete object files to the recipients so that they can relink them +with the library, after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + Our method of protecting your rights has two steps: (1) copyright +the library, and (2) offer you this license which gives you legal +permission to copy, distribute and/or modify the library. + + Also, for each distributor's protection, we want to make certain +that everyone understands that there is no warranty for this free +library. If the library is modified by someone else and passed on, we +want its recipients to know that what they have is not the original +version, 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 companies distributing free +software will individually obtain patent licenses, thus in effect +transforming the program into proprietary software. To prevent this, +we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + + Most GNU software, including some libraries, is covered by the ordinary +GNU General Public License, which was designed for utility programs. This +license, the GNU Library General Public License, applies to certain +designated libraries. This license is quite different from the ordinary +one; be sure to read it in full, and don't assume that anything in it is +the same as in the ordinary license. + + The reason we have a separate public license for some libraries is that +they blur the distinction we usually make between modifying or adding to a +program and simply using it. Linking a program with a library, without +changing the library, is in some sense simply using the library, and is +analogous to running a utility program or application program. However, in +a textual and legal sense, the linked executable is a combined work, a +derivative of the original library, and the ordinary General Public License +treats it as such. + + Because of this blurred distinction, using the ordinary General +Public License for libraries did not effectively promote software +sharing, because most developers did not use the libraries. We +concluded that weaker conditions might promote sharing better. + + However, unrestricted linking of non-free programs would deprive the +users of those programs of all benefit from the free status of the +libraries themselves. This Library General Public License is intended to +permit developers of non-free programs to use free libraries, while +preserving your freedom as a user of such programs to change the free +libraries that are incorporated in them. (We have not seen how to achieve +this as regards changes in header files, but we have achieved it as regards +changes in the actual functions of the Library.) The hope is that this +will lead to faster development of free libraries. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, while the latter only +works together with the library. + + Note that it is possible for a library to be covered by the ordinary +General Public License rather than by this special one. + + GNU LIBRARY GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library which +contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Library +General Public License (also called "this License"). Each licensee is +addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, 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 library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete 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 distribute a copy of this License along with the +Library. + + 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 Library or any portion +of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +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 Library, 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 Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you 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. + + If distribution of 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 satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also compile or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + c) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + d) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. 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. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library 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. + + 9. 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 Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +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. + + 11. 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 Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library 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 Library. + +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. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library 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. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Library 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 Library +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 Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +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 + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. 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 LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. 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 library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..004a700 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,19 @@ +2005-09-10 Christophe Fergeau <teuf@gnome.org> + + * AUTHORS: + * COPYING: + * INSTALL: + * Makefile.am: + * autogen.sh: + * configure.ac: + * libgpod-1.0.pc.in: + * po/Makefile.in.in: + * po/POTFILES.in: + * src/Makefile.am: + * src/itdb.h: + * src/itdb_itunesdb.c: + * src/itdb_playlist.c: + * src/itdb_private.h: + * src/itdb_track.c: + * tests/Makefile.am: + * tests/itdb_main.c: initial import @@ -0,0 +1,229 @@ +Copyright 1994, 1995, 1996, 1999, 2000, 2001, 2002 Free Software +Foundation, Inc. + + This file is free documentation; the Free Software Foundation gives +unlimited permission to copy, distribute and modify it. + +Basic Installation +================== + + These are generic installation instructions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, and a +file `config.log' containing compiler output (useful mainly for +debugging `configure'). + + It can also use an optional file (typically called `config.cache' +and enabled with `--cache-file=config.cache' or simply `-C') that saves +the results of its tests to speed up reconfiguring. (Caching is +disabled by default to prevent problems with accidental use of stale +cache files.) + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If you are using the cache, and at +some point `config.cache' contains results you don't want to keep, you +may remove or edit it. + + The file `configure.ac' (or `configure.in') is used to create +`configure' by a program called `autoconf'. You only need +`configure.ac' if you want to change it or regenerate `configure' using +a newer version of `autoconf'. + +The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. If you're + using `csh' on an old version of System V, you might need to type + `sh ./configure' instead to prevent `csh' from trying to execute + `configure' itself. + + Running `configure' takes awhile. While running, it prints some + messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package. + + 4. Type `make install' to install the programs and any data files and + documentation. + + 5. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. Run `./configure --help' +for details on some of the pertinent environment variables. + + You can give `configure' initial values for configuration parameters +by setting variables in the command line or in the environment. Here +is an example: + + ./configure CC=c89 CFLAGS=-O2 LIBS=-lposix + + *Note Defining Variables::, for more details. + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you must use a version of `make' that +supports the `VPATH' variable, such as GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. + + If you have to use a `make' that does not support the `VPATH' +variable, you have to compile the package for one architecture at a +time in the source code directory. After you have installed the +package for one architecture, use `make distclean' before reconfiguring +for another architecture. + +Installation Names +================== + + By default, `make install' will install the package's files in +`/usr/local/bin', `/usr/local/man', etc. You can specify an +installation prefix other than `/usr/local' by giving `configure' the +option `--prefix=PATH'. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +give `configure' the option `--exec-prefix=PATH', the package will use +PATH as the prefix for installing programs and libraries. +Documentation and other data files will still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=PATH' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + +Optional Features +================= + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + +Specifying the System Type +========================== + + There may be some features `configure' cannot figure out +automatically, but needs to determine by the type of machine the package +will run on. Usually, assuming the package is built to be run on the +_same_ architectures, `configure' can figure that out, but if it prints +a message saying it cannot guess the machine type, give it the +`--build=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name which has the form: + + CPU-COMPANY-SYSTEM + +where SYSTEM can have one of these forms: + + OS KERNEL-OS + + See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the machine type. + + If you are _building_ compiler tools for cross-compiling, you should +use the `--target=TYPE' option to select the type of system they will +produce code for. + + If you want to _use_ a cross compiler, that generates code for a +platform different from the build platform, you should specify the +"host" platform (i.e., that on which the generated programs will +eventually be run) with `--host=TYPE'. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Defining Variables +================== + + Variables not defined in a site shell script can be set in the +environment passed to `configure'. However, some packages may run +configure again during the build, and the customized values of these +variables may be lost. In order to avoid this problem, you should set +them in the `configure' command line, using `VAR=value'. For example: + + ./configure CC=/usr/local2/bin/gcc + +will cause the specified gcc to be used as the C compiler (unless it is +overridden in the site shell script). + +`configure' Invocation +====================== + + `configure' recognizes the following options to control how it +operates. + +`--help' +`-h' + Print a summary of the options to `configure', and exit. + +`--version' +`-V' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`--cache-file=FILE' + Enable the cache: use and save the results of the tests in FILE, + traditionally `config.cache'. FILE defaults to `/dev/null' to + disable caching. + +`--config-cache' +`-C' + Alias for `--cache-file=config.cache'. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`configure' also accepts some other, not widely useful, options. Run +`configure --help' for more details. + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..76e7436 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,16 @@ +SUBDIRS=src tests po + +pkgconfigdir = $(libdir)/pkgconfig +pkgconfig_DATA = libgpod-1.0.pc + +EXTRA_DIST = \ + intltool-merge.in \ + intltool-update.in \ + intltool-extract.in \ + libgpod-1.0.pc.in + +DISTCLEANFILES = \ + intltool-extract \ + intltool-merge \ + intltool-update + diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..b0daa2f --- /dev/null +++ b/autogen.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +srcdir=`dirname $0` +test -z "$srcdir" && srcdir=. + +PKG_NAME="libgpod" + +(test -f $srcdir/configure.ac) || { + echo -n "**Error**: Directory "\`$srcdir\'" does not look like the" + echo " top-level $PKG_NAME directory" + exit 1 +} + +which gnome-autogen.sh || { + echo "You need to install gnome-common from the GNOME CVS" + exit 1 +} + +REQUIRED_AUTOMAKE_VERSION=1.8 USE_GNOME2_MACROS=1 . gnome-autogen.sh diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..6a5ef92 --- /dev/null +++ b/configure.ac @@ -0,0 +1,120 @@ +AC_PREREQ(2.52) +AC_INIT(src) +AC_CONFIG_SRCDIR(configure.ac) + +AM_CONFIG_HEADER(config.h) + +# Making releases: +# LIBGPOD_MICRO_VERSION += 1; +# LIBGPOD_INTERFACE_AGE += 1; +# if any functions have been added, set LIBGPOD_INTERFACE_AGE to 0. +# if backwards compatibility has been broken, +# set LIBGPOD_BINARY_AGE and LIBGPOD_INTERFACE_AGE to 0. +# +LIBGPOD_MAJOR_VERSION=0 +LIBGPOD_MINOR_VERSION=1 +LIBGPOD_MICRO_VERSION=1 +LIBGPOD_INTERFACE_AGE=1 +# If you need a modifier for the version number. +# Normally empty, but can be used to make "fixup" releases. +LIBGPOD_EXTRAVERSION= + +dnl libtool versioning from libgnome + +LIBGPOD_CURRENT=`expr 100 '*' $LIBGPOD_MINOR_VERSION + $LIBGPOD_MICRO_VERSION - $LIBGPOD_INTERFACE_AGE` +LIBGPOD_BINARY_AGE=`expr 100 '*' $LIBGPOD_MINOR_VERSION + $LIBGPOD_MICRO_VERSION` +LIBGPOD_REVISION=$LIBGPOD_INTERFACE_AGE +LIBGPOD_AGE=`expr $LIBGPOD_BINARY_AGE - $LIBGPOD_INTERFACE_AGE` +LIBGPOD_VERSION=$LIBGPOD_MAJOR_VERSION.$LIBGPOD_MINOR_VERSION.$LIBGPOD_MICRO_VERSION$LIBGPOD_EXTRAVERSION + +AC_SUBST(LIBGPOD_CURRENT) +AC_SUBST(LIBGPOD_REVISION) +AC_SUBST(LIBGPOD_AGE) +AC_SUBST(LIBGPOD_VERSION) + +AM_INIT_AUTOMAKE(libgpod, $LIBGPOD_VERSION) + +dnl make sure we keep ACLOCAL_FLAGS around for maintainer builds to work +AC_SUBST(ACLOCAL_AMFLAGS, "$ACLOCAL_FLAGS") + +AM_MAINTAINER_MODE + +AC_PROG_CC +AC_PROG_LD +AC_STDC_HEADERS +AC_PROG_INSTALL +AC_PROG_LIBTOOL +AC_PROG_LN_S +AC_PROG_MAKE_SET +AC_PROG_INTLTOOL([0.21]) + +PKG_CHECK_MODULES(LIBGPOD, glib-2.0) +LIBGPOD_CFLAGS="$LIBGPOD_CFLAGS -Wall" +AC_SUBST(LIBGPOD_CFLAGS) +AC_SUBST(LIBGPOD_LIBS) + +dnl ************************************************** +dnl * internationalization support +dnl ************************************************** +ALL_LINGUAS="" + +GETTEXT_PACKAGE=libgpod +AC_SUBST(GETTEXT_PACKAGE) +AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE,"$GETTEXT_PACKAGE", [Gettext package.]) + +AM_GLIB_GNU_GETTEXT + + +dnl warnings bits, copied from gnome-keyring configure.in +dnl Turn on the additional warnings last, so -Werror doesn't affect other tests. + +AC_ARG_ENABLE(more-warnings, +[ --enable-more-warnings Maximum compiler warnings], +set_more_warnings="$enableval",[ +if test -d "$srcdir/{arch}" || test -d "$srcdir/CVS"; then + set_more_warnings=yes +else + set_more_warnings=no +fi +]) +AC_MSG_CHECKING(for more warnings, including -Werror) +if test "$GCC" = "yes" -a "$set_more_warnings" != "no"; then + AC_MSG_RESULT(yes) + CFLAGS="\ + -Wall \ + -Wchar-subscripts -Wmissing-declarations -Wmissing-prototypes \ + -Wnested-externs -Wpointer-arith \ + -Wcast-align -Wsign-compare \ + -Werror -std=c89 \ + $CFLAGS" + + for option in -Wno-strict-aliasing -Wno-sign-compare -Wdeclaration-after-statement; do + SAVE_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS $option" + AC_MSG_CHECKING([whether gcc understands $option]) + AC_TRY_COMPILE([], [], + has_option=yes, + has_option=no,) + if test $has_option = no; then + CFLAGS="$SAVE_CFLAGS" + fi + AC_MSG_RESULT($has_option) + unset has_option + unset SAVE_CFLAGS + done + unset option +else + AC_MSG_RESULT(no) +fi + +AC_SUBST(CFLAGS) +AC_SUBST(CPPFLAGS) +AC_SUBST(LDFLAGS) + +AC_OUTPUT([ +Makefile +po/Makefile.in +src/Makefile +tests/Makefile +libgpod-1.0.pc +]) diff --git a/libgpod-1.0.pc.in b/libgpod-1.0.pc.in new file mode 100644 index 0000000..450c656 --- /dev/null +++ b/libgpod-1.0.pc.in @@ -0,0 +1,11 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: libgpod +Description: A library to manipulate songs and playlists stored on an ipod +Version: @VERSION@ +Requires: glib-2.0 +Libs: -L${libdir} -lgpod +Cflags: -I${includedir}/gpod-1.0 diff --git a/po/ChangeLog b/po/ChangeLog new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/po/ChangeLog diff --git a/po/Makefile.in.in b/po/Makefile.in.in new file mode 100644 index 0000000..1a6961e --- /dev/null +++ b/po/Makefile.in.in @@ -0,0 +1,263 @@ +# Makefile for program source directory in GNU NLS utilities package. +# Copyright (C) 1995, 1996, 1997 by Ulrich Drepper <drepper@gnu.ai.mit.edu> +# +# This file file be copied and used freely without restrictions. It can +# be used in projects which are not available under the GNU Public License +# but which still want to provide support for the GNU gettext functionality. +# Please note that the actual code is *not* freely available. +# +# - Modified by Owen Taylor <otaylor@redhat.com> to use GETTEXT_PACKAGE +# instead of PACKAGE and to look for po2tbl in ./ not in intl/ +# +# - Modified by jacob berkman <jacob@ximian.com> to install +# Makefile.in.in and po2tbl.sed.in for use with glib-gettextize +# +# - Modified by Rodney Dawes <dobey@novell.com> for use with intltool +# +# We have the following line for use by intltoolize: +# INTLTOOL_MAKEFILE + +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +PACKAGE = @PACKAGE@ +VERSION = @VERSION@ + +SHELL = /bin/sh +@SET_MAKE@ + +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +top_builddir = .. +VPATH = @srcdir@ + +prefix = @prefix@ +exec_prefix = @exec_prefix@ +datadir = @datadir@ +datarootdir = @datarootdir@ +libdir = @libdir@ +localedir = $(libdir)/locale +gnulocaledir = $(datadir)/locale +gettextsrcdir = $(datadir)/glib-2.0/gettext/po +subdir = po +install_sh = @install_sh@ +mkinstalldirs = $(SHELL) $(top_srcdir)/mkinstalldirs + +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ + +CC = @CC@ +GENCAT = @GENCAT@ +GMSGFMT = @GMSGFMT@ +MSGFMT = @MSGFMT@ +XGETTEXT = @XGETTEXT@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +MSGMERGE = INTLTOOL_EXTRACT=$(INTLTOOL_EXTRACT) srcdir=$(srcdir) $(INTLTOOL_UPDATE) --gettext-package $(GETTEXT_PACKAGE) --dist +GENPOT = INTLTOOL_EXTRACT=$(INTLTOOL_EXTRACT) srcdir=$(srcdir) $(INTLTOOL_UPDATE) --gettext-package $(GETTEXT_PACKAGE) --pot + +DEFS = @DEFS@ +CFLAGS = @CFLAGS@ +CPPFLAGS = @CPPFLAGS@ + +INCLUDES = -I.. -I$(top_srcdir)/intl + +COMPILE = $(CC) -c $(DEFS) $(INCLUDES) $(CPPFLAGS) $(CFLAGS) $(XCFLAGS) + +SOURCES = +POFILES = @POFILES@ +GMOFILES = @GMOFILES@ +DISTFILES = ChangeLog Makefile.in.in POTFILES.in \ +$(POFILES) $(SOURCES) +EXTRA_DISTFILES = POTFILES.skip Makevars LINGUAS + +POTFILES = \ + +CATALOGS = @CATALOGS@ +CATOBJEXT = @CATOBJEXT@ +INSTOBJEXT = @INSTOBJEXT@ + +.SUFFIXES: +.SUFFIXES: .c .o .po .pox .gmo .mo .msg .cat + +.c.o: + $(COMPILE) $< + +.po.pox: + $(MAKE) $(GETTEXT_PACKAGE).pot + $(MSGMERGE) $< $(GETTEXT_PACKAGE).pot -o $*.pox + +.po.mo: + $(MSGFMT) -o $@ $< + +.po.gmo: + file=`echo $* | sed 's,.*/,,'`.gmo \ + && rm -f $$file && $(GMSGFMT) -o $$file $< + +.po.cat: + sed -f ../intl/po2msg.sed < $< > $*.msg \ + && rm -f $@ && $(GENCAT) $@ $*.msg + + +all: all-@USE_NLS@ + +all-yes: $(CATALOGS) +all-no: + +$(GETTEXT_PACKAGE).pot: $(POTFILES) + $(GENPOT) + +install: install-exec install-data +install-exec: +install-data: install-data-@USE_NLS@ +install-data-no: all +install-data-yes: all + if test -n "$(MKINSTALLDIRS)"; then \ + $(MKINSTALLDIRS) $(DESTDIR)$(datadir); \ + else \ + $(SHELL) $(top_srcdir)/mkinstalldirs $(DESTDIR)$(datadir); \ + fi + @catalogs='$(CATALOGS)'; \ + for cat in $$catalogs; do \ + cat=`basename $$cat`; \ + case "$$cat" in \ + *.gmo) destdir=$(gnulocaledir);; \ + *) destdir=$(localedir);; \ + esac; \ + lang=`echo $$cat | sed 's/\$(CATOBJEXT)$$//'`; \ + dir=$(DESTDIR)$$destdir/$$lang/LC_MESSAGES; \ + if test -n "$(MKINSTALLDIRS)"; then \ + $(MKINSTALLDIRS) $$dir; \ + else \ + $(SHELL) $(top_srcdir)/mkinstalldirs $$dir; \ + fi; \ + if test -r $$cat; then \ + $(INSTALL_DATA) $$cat $$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT); \ + echo "installing $$cat as $$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT)"; \ + else \ + $(INSTALL_DATA) $(srcdir)/$$cat $$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT); \ + echo "installing $(srcdir)/$$cat as" \ + "$$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT)"; \ + fi; \ + if test -r $$cat.m; then \ + $(INSTALL_DATA) $$cat.m $$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT).m; \ + echo "installing $$cat.m as $$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT).m"; \ + else \ + if test -r $(srcdir)/$$cat.m ; then \ + $(INSTALL_DATA) $(srcdir)/$$cat.m \ + $$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT).m; \ + echo "installing $(srcdir)/$$cat as" \ + "$$dir/$(GETTEXT_PACKAGE)$(INSTOBJEXT).m"; \ + else \ + true; \ + fi; \ + fi; \ + done + if test "$(PACKAGE)" = "glib"; then \ + if test -n "$(MKINSTALLDIRS)"; then \ + $(MKINSTALLDIRS) $(DESTDIR)$(gettextsrcdir); \ + else \ + $(SHELL) $(top_srcdir)/mkinstalldirs $(DESTDIR)$(gettextsrcdir); \ + fi; \ + $(INSTALL_DATA) $(srcdir)/Makefile.in.in \ + $(DESTDIR)$(gettextsrcdir)/Makefile.in.in; \ + else \ + : ; \ + fi + +# Define this as empty until I found a useful application. +installcheck: + +uninstall: + catalogs='$(CATALOGS)'; \ + for cat in $$catalogs; do \ + cat=`basename $$cat`; \ + lang=`echo $$cat | sed 's/\$(CATOBJEXT)$$//'`; \ + rm -f $(DESTDIR)$(localedir)/$$lang/LC_MESSAGES/$(GETTEXT_PACKAGE)$(INSTOBJEXT); \ + rm -f $(DESTDIR)$(localedir)/$$lang/LC_MESSAGES/$(GETTEXT_PACKAGE)$(INSTOBJEXT).m; \ + rm -f $(DESTDIR)$(gnulocaledir)/$$lang/LC_MESSAGES/$(GETTEXT_PACKAGE)$(INSTOBJEXT); \ + rm -f $(DESTDIR)$(gnulocaledir)/$$lang/LC_MESSAGES/$(GETTEXT_PACKAGE)$(INSTOBJEXT).m; \ + done + if test "$(PACKAGE)" = "glib"; then \ + rm -f $(DESTDIR)$(gettextsrcdir)/Makefile.in.in; \ + fi + +check: all $(GETTEXT_PACKAGE).pot + +dvi info tags TAGS ID: + +mostlyclean: + rm -f core core.* *.pox $(GETTEXT_PACKAGE).pot *.old.po cat-id-tbl.tmp + rm -fr *.o + rm -f .intltool-merge-cache + +clean: mostlyclean + +distclean: clean + rm -f Makefile Makefile.in POTFILES + rm -f *.mo *.msg *.cat *.cat.m $(GMOFILES) + +maintainer-clean: distclean + @echo "This command is intended for maintainers to use;" + @echo "it deletes files that may require special tools to rebuild." + rm -f Makefile.in.in + +distdir = ../$(GETTEXT_PACKAGE)-$(VERSION)/$(subdir) +dist distdir: $(DISTFILES) + dists="$(DISTFILES)"; \ + extra_dists="$(EXTRA_DISTFILES)"; \ + for file in $$extra_dists; do \ + test -f $$file && dists="$$dists $$file"; \ + done; \ + for file in $$dists; do \ + ln $(srcdir)/$$file $(distdir) 2> /dev/null \ + || cp -p $(srcdir)/$$file $(distdir); \ + done + +update-po: Makefile + $(MAKE) $(GETTEXT_PACKAGE).pot + tmpdir=`pwd`; \ + catalogs='$(CATALOGS)'; \ + for cat in $$catalogs; do \ + cat=`basename $$cat`; \ + lang=`echo $$cat | sed 's/\$(CATOBJEXT)$$//'`; \ + echo "$$lang:"; \ + result="`$(MSGMERGE) -o $$tmpdir/$$lang.new.po $$lang`"; \ + if $$result; then \ + if cmp $(srcdir)/$$lang.po $$tmpdir/$$lang.new.po >/dev/null 2>&1; then \ + rm -f $$tmpdir/$$lang.new.po; \ + else \ + if mv -f $$tmpdir/$$lang.new.po $$lang.po; then \ + :; \ + else \ + echo "msgmerge for $$lang.po failed: cannot move $$tmpdir/$$lang.new.po to $$lang.po" 1>&2; \ + rm -f $$tmpdir/$$lang.new.po; \ + exit 1; \ + fi; \ + fi; \ + else \ + echo "msgmerge for $$cat failed!"; \ + rm -f $$tmpdir/$$lang.new.po; \ + fi; \ + done + +# POTFILES is created from POTFILES.in by stripping comments, empty lines +# and Intltool tags (enclosed in square brackets), and appending a full +# relative path to them +POTFILES: POTFILES.in + ( posrcprefix='$(top_srcdir)/'; \ + rm -f $@-t $@ \ + && (sed -e '/^#/d' \ + -e 's/^[[].*] *//' \ + -e '/^[ ]*$$/d' \ + -e "s@^@ $$posrcprefix@" $(srcdir)/$@.in \ + | sed -e '$$!s/$$/ \\/') > $@-t \ + && chmod a-w $@-t \ + && mv $@-t $@ ) + +Makefile: Makefile.in.in ../config.status POTFILES + cd .. \ + && CONFIG_FILES=$(subdir)/$@.in CONFIG_HEADERS= \ + $(SHELL) ./config.status + +# Tell versions [3.59,3.63) of GNU make not to export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..f1261ee --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,2 @@ +src/itdb_itunesdb.c +src/itdb_playlist.c diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..e0998fd --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,18 @@ +lib_LTLIBRARIES = libgpod.la + +libgpod_la_SOURCES = \ + itdb.h \ + itdb_itunesdb.c \ + itdb_playlist.c \ + itdb_private.h \ + itdb_track.c +libgpod_la_headers = itdb.h +libgpod_la_noinst_headers = itdb_private.h +libgpod_la_LDFLAGS = -version-info $(LIBGPOD_CURRENT):$(LIBGPOD_REVISION):$(LIBGPOD_AGE) \ + -no-undefined + +libgpodincludedir = $(includedir)/gpod-1.0/gpod +libgpodinclude_HEADERS = $(libgpod_la_headers) + +INCLUDES=$(LIBGPOD_CFLAGS) +LIBS=$(LIBGPOD_LIBS) diff --git a/src/itdb.h b/src/itdb.h new file mode 100644 index 0000000..9dc67e5 --- /dev/null +++ b/src/itdb.h @@ -0,0 +1,504 @@ +/* Time-stamp: <2005-06-17 22:25:30 jcs> +| +| Copyright (C) 2002-2005 Jorg Schuler <jcsjcs at users sourceforge net> +| Part of the gtkpod project. +| +| URL: http://www.gtkpod.org/ +| URL: http://gtkpod.sourceforge.net/ +| +| Most of the code in this file has been ported from the perl +| script "mktunes.pl" (part of the gnupod-tools collection) written +| by Adrian Ulrich <pab at blinkenlights.ch>. +| +| gnupod-tools: http://www.blinkenlights.ch/cgi-bin/fm.pl?get=ipod +| +| The code contained in this file is free software; you can redistribute +| it and/or modify it under the terms of the GNU Lesser General Public +| License as published by the Free Software Foundation; either version +| 2.1 of the License, or (at your option) any later version. +| +| This file 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 +| Lesser General Public License for more details. +| +| You should have received a copy of the GNU Lesser General Public +| License along with this code; if not, write to the Free Software +| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +| $Id$ +*/ + +#ifndef __ITUNESDB_H__ +#define __ITUNESDB_H__ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <time.h> +#include <glib.h> + + +/* one star is how much (track->rating) */ +#define ITDB_RATING_STEP 20 + +enum ItdbPlType { /* types for playlist->type */ + ITDB_PL_TYPE_NORM = 0, /* normal playlist, visible in iPod */ + ITDB_PL_TYPE_MPL = 1 /* master playlist, contains all tracks, + not visible in iPod */ +}; + + + +/* Most of the knowledge about smart playlists has been provided by + Samuel "Otto" Wood (sam dot wood at gmail dot com) who let me dig + in his impressive C++ class. Contact him for a complete + copy. Further, all enums and #defines below, SPLRule, SPLRules, and + SPLPref may also be used under a FreeBSD license. */ + +#define SPL_STRING_MAXLEN 255 + +/* Definitions for smart playlists */ +enum { /* types for match_operator */ + SPLMATCH_AND = 0, /* AND rule - all of the rules must be true in + order for the combined rule to be applied */ + SPLMATCH_OR = 1 /* OR rule */ +}; + +/* Limit Types.. like limit playlist to 100 minutes or to 100 songs */ +enum { + LIMITTYPE_MINUTES = 0x01, + LIMITTYPE_MB = 0x02, + LIMITTYPE_SONGS = 0x03, + LIMITTYPE_HOURS = 0x04, + LIMITTYPE_GB = 0x05 +}; + +/* Limit Sorts.. Like which songs to pick when using a limit type + Special note: the values for LIMITSORT_LEAST_RECENTLY_ADDED, + LIMITSORT_LEAST_OFTEN_PLAYED, LIMITSORT_LEAST_RECENTLY_PLAYED, and + LIMITSORT_LOWEST_RATING are really 0x10, 0x14, 0x15, 0x17, with the + 'limitsort_opposite' flag set. This is the same value as the + "positive" value (i.e. LIMITSORT_LEAST_RECENTLY_ADDED), and is + really very terribly awfully weird, so we map the values to iPodDB + specific values with the high bit set. + + On writing, we check the high bit and write the limitsort_opposite + from that. That way, we don't have to deal with programs using the + class needing to set the wrong limit and then make it into the + "opposite", which would be frickin' annoying. */ +enum { + LIMITSORT_RANDOM = 0x02, + LIMITSORT_SONG_NAME = 0x03, + LIMITSORT_ALBUM = 0x04, + LIMITSORT_ARTIST = 0x05, + LIMITSORT_GENRE = 0x07, + LIMITSORT_MOST_RECENTLY_ADDED = 0x10, + LIMITSORT_LEAST_RECENTLY_ADDED = 0x80000010, /* See note above */ + LIMITSORT_MOST_OFTEN_PLAYED = 0x14, + LIMITSORT_LEAST_OFTEN_PLAYED = 0x80000014, /* See note above */ + LIMITSORT_MOST_RECENTLY_PLAYED = 0x15, + LIMITSORT_LEAST_RECENTLY_PLAYED = 0x80000015,/* See note above */ + LIMITSORT_HIGHEST_RATING = 0x17, + LIMITSORT_LOWEST_RATING = 0x80000017, /* See note above */ +}; + +/* Smartlist Actions - Used in the rules. +Note by Otto (Samuel Wood): + really this is a bitmapped field... + high byte + bit 0 = "string" values if set, "int" values if not set + bit 1 = "not", or to negate the check. + lower 2 bytes + bit 0 = simple "IS" query + bit 1 = contains + bit 2 = begins with + bit 3 = ends with + bit 4 = greater than + bit 5 = unknown, but probably greater than or equal to + bit 6 = less than + bit 7 = unknown, but probably less than or equal to + bit 8 = a range selection + bit 9 = "in the last" +*/ +typedef enum { + SPLACTION_IS_INT = 0x00000001, /* "Is Set" in iTunes */ + SPLACTION_IS_GREATER_THAN = 0x00000010, /* "Is After" in iTunes */ + SPLACTION_IS_LESS_THAN = 0x00000040, /* "Is Before" in iTunes */ + SPLACTION_IS_IN_THE_RANGE = 0x00000100, + SPLACTION_IS_IN_THE_LAST = 0x00000200, + + SPLACTION_IS_STRING = 0x01000001, + SPLACTION_CONTAINS = 0x01000002, + SPLACTION_STARTS_WITH = 0x01000004, + SPLACTION_ENDS_WITH = 0x01000008, + + SPLACTION_IS_NOT_INT = 0x02000001, /* "Is Not Set" in iTunes */ + + /* Note: Not available in iTunes 4.5 (untested on iPod) */ + SPLACTION_IS_NOT_GREATER_THAN = 0x02000010, + /* Note: Not available in iTunes 4.5 (untested on iPod) */ + SPLACTION_IS_NOT_LESS_THAN = 0x02000040, + /* Note: Not available in iTunes 4.5 (seems to work on iPod) */ + SPLACTION_IS_NOT_IN_THE_RANGE = 0x02000100, + + SPLACTION_IS_NOT_IN_THE_LAST = 0x02000200, + SPLACTION_IS_NOT = 0x03000001, + SPLACTION_DOES_NOT_CONTAIN = 0x03000002, + + /* Note: Not available in iTunes 4.5 (seems to work on iPod) */ + SPLACTION_DOES_NOT_START_WITH = 0x03000004, + /* Note: Not available in iTunes 4.5 (seems to work on iPod) */ + SPLACTION_DOES_NOT_END_WITH = 0x03000008, +} SPLAction; + +typedef enum +{ + splft_string = 1, + splft_int, + splft_boolean, + splft_date, + splft_playlist, + splft_unknown +} SPLFieldType; + +typedef enum +{ + splat_string = 1, + splat_int, + splat_date, + splat_range_int, + splat_range_date, + splat_inthelast, + splat_playlist, + splat_none, + splat_invalid, + splat_unknown +} SPLActionType; + + +/* These are to pass to AddRule() when you need a unit for the two "in + the last" action types Or, in theory, you can use any time + range... iTunes might not like it, but the iPod shouldn't care. */ +enum { + SPLACTION_LAST_DAYS_VALUE = 86400, /* nr of secs in 24 hours */ + SPLACTION_LAST_WEEKS_VALUE = 604800, /* nr of secs in 7 days */ + SPLACTION_LAST_MONTHS_VALUE = 2628000,/* nr of secs in 30.4167 + days ~= 1 month */ +} ; + +#if 0 +// Hey, why limit ourselves to what iTunes can do? If the iPod can deal with it, excellent! +#define SPLACTION_LAST_HOURS_VALUE 3600 // number of seconds in 1 hour +#define SPLACTION_LAST_MINUTES_VALUE 60 // number of seconds in 1 minute +#define SPLACTION_LAST_YEARS_VALUE 31536000 // number of seconds in 365 days + +// fun ones.. Near as I can tell, all of these work. It's open like that. :) +#define SPLACTION_LAST_LUNARCYCLE_VALUE 2551443 // a "lunar cycle" is the time it takes the moon to circle the earth +#define SPLACTION_LAST_SIDEREAL_DAY 86164 // a "sidereal day" is time in one revolution of earth on its axis +#define SPLACTION_LAST_SWATCH_BEAT 86 // a "swatch beat" is 1/1000th of a day.. search for "internet time" on google +#define SPLACTION_LAST_MOMENT 90 // a "moment" is 1/40th of an hour, or 1.5 minutes +#define SPLACTION_LAST_OSTENT 600 // an "ostent" is 1/10th of an hour, or 6 minutes +#define SPLACTION_LAST_FORTNIGHT 1209600 // a "fortnight" is 14 days +#define SPLACTION_LAST_VINAL 1728000 // a "vinal" is 20 days +#define SPLACTION_LAST_QUARTER 7889231 // a "quarter" is a quarter year +#define SPLACTION_LAST_SOLAR_YEAR 31556926 // a "solar year" is the time it takes the earth to go around the sun +#define SPLACTION_LAST_SIDEREAL_YEAR 31558150 // a "sidereal year" is the time it takes the earth to reach the same point in space again, compared to the stars +#endif + +/* Smartlist fields - Used for rules. */ +typedef enum { + SPLFIELD_SONG_NAME = 0x02, /* String */ + SPLFIELD_ALBUM = 0x03, /* String */ + SPLFIELD_ARTIST = 0x04, /* String */ + SPLFIELD_BITRATE = 0x05, /* Int (e.g. from/to = 128) */ + SPLFIELD_SAMPLE_RATE = 0x06, /* Int (e.g. from/to = 44100) */ + SPLFIELD_YEAR = 0x07, /* Int (e.g. from/to = 2004) */ + SPLFIELD_GENRE = 0x08, /* String */ + SPLFIELD_KIND = 0x09, /* String */ + SPLFIELD_DATE_MODIFIED = 0x0a,/* Int/Mac Timestamp (e.g. from/to = + bcf93280 == is before 6/19/2004)*/ + SPLFIELD_TRACKNUMBER = 0x0b, /* Int (e.g. from = 1, to = 2) */ + SPLFIELD_SIZE = 0x0c, /* Int (e.g. from/to = 0x00600000 + for 6MB) */ + SPLFIELD_TIME = 0x0d, /* Int (e.g. from/to = 83999 for + 1:23/83 seconds) */ + SPLFIELD_COMMENT = 0x0e, /* String */ + SPLFIELD_DATE_ADDED = 0x10, /* Int/Mac Timestamp (e.g. from/to = + bcfa83ff == is after 6/19/2004) */ + SPLFIELD_COMPOSER = 0x12, /* String */ + SPLFIELD_PLAYCOUNT = 0x16, /* Int (e.g. from/to = 1) */ + SPLFIELD_LAST_PLAYED = 0x17, /* Int/Mac Timestamp (e.g. from = + bcfa83ff (6/19/2004) to = + 0xbcfbd57f (6/20/2004)) */ + SPLFIELD_DISC_NUMBER = 0x18, /* Int (e.g. from/to = 1) */ + SPLFIELD_RATING = 0x19, /* Int/Stars Rating (e.g. from/to = + 60 (3 stars)) */ + SPLFIELD_COMPILATION = 0x1f, /* Int (e.g. is set -> + SPLACTION_IS_INT/from=1, + is not set -> + SPLACTION_IS_NOT_INT/from=1) */ + SPLFIELD_BPM = 0x23, /* Int (e.g. from/to = 60) */ + SPLFIELD_GROUPING = 0x27, /* String */ + SPLFIELD_PLAYLIST = 0x28, /* XXX - Unknown...not parsed + correctly...from/to = 0xb6fbad5f + for * "Purchased Music". Extra + data after * "to"... */ +} SPLField; + +#define SPLDATE_IDENTIFIER (G_GINT64_CONSTANT (0x2dae2dae2dae2daeU)) + +/* Maximum string length that iTunes writes to the database */ +#define SPL_MAXSTRINGLENGTH 255 + +typedef struct SPLPref +{ + guint8 liveupdate; /* "live Updating" check box */ + guint8 checkrules; /* "Match X of the following + conditions" check box */ + guint8 checklimits; /* "Limit To..." check box */ + guint32 limittype; /* See types defined above */ + guint32 limitsort; /* See types defined above */ + guint32 limitvalue; /* The value typed next to "Limit + type" */ + guint8 matchcheckedonly; /* "Match only checked songs" check + box */ +} SPLPref; + +typedef struct SPLRule +{ + guint32 field; + guint32 action; + gchar *string; /* data in UTF8 */ + /* from and to are pretty stupid.. if it's a date type of field, + then + value = 0x2dae2dae2dae2dae, + date = some number, like 2 or -2 + units = unit in seconds, like 604800 = a week + but if this is actually some kind of integer comparison, like + rating = 60 (3 stars) + value = the value we care about + date = 0 + units = 1 */ + guint64 fromvalue; + gint64 fromdate; + guint64 fromunits; + guint64 tovalue; + gint64 todate; + guint64 tounits; + guint32 unk052; + guint32 unk056; + guint32 unk060; + guint32 unk064; + guint32 unk068; +} SPLRule; + + +typedef struct SPLRules +{ + guint32 unk004; + guint32 match_operator; /* "All" (logical AND): SPLMATCH_AND, + "Any" (logical OR): SPLMATCH_OR */ + GList *rules; +} SPLRules; + + +typedef void (* ItdbUserDataDestroyFunc) (gpointer userdata); +typedef gpointer (* ItdbUserDataDuplicateFunc) (gpointer userdata); + +typedef struct +{ + GList *tracks; + GList *playlists; + gchar *filename; /* filename of iTunesDB */ + gchar *mountpoint; /* mountpoint of iPod (if available) */ + guint32 version; + guint64 id; + /* below is for use by application */ + guint64 usertype; + gpointer userdata; + /* function called to duplicate userdata */ + ItdbUserDataDuplicateFunc userdata_duplicate; + /* function called to free userdata */ + ItdbUserDataDestroyFunc userdata_destroy; +} Itdb_iTunesDB; + + +typedef struct +{ + Itdb_iTunesDB *itdb; /* pointer to iTunesDB (for convenience) */ + gchar *name; /* name of playlist in UTF8 */ + guint32 type; /* PL_TYPE_MPL: master play list */ + gint num; /* number of tracks in playlist */ + GList *members; /* tracks in playlist (Track *) */ + gboolean is_spl; /* smart playlist? */ + guint32 timestamp; /* some timestamp */ + guint64 id; /* playlist ID */ + guint32 unk036, unk040, unk044; + SPLPref splpref; /* smart playlist prefs */ + SPLRules splrules; /* rules for smart playlists */ + /* below is for use by application */ + guint64 usertype; + gpointer userdata; + /* function called to duplicate userdata */ + ItdbUserDataDuplicateFunc userdata_duplicate; + /* function called to free userdata */ + ItdbUserDataDestroyFunc userdata_destroy; +} Itdb_Playlist; + + +typedef struct +{ + Itdb_iTunesDB *itdb; /* pointer to iTunesDB (for convenience) */ + gchar *album; /* album (utf8) */ + gchar *artist; /* artist (utf8) */ + gchar *title; /* title (utf8) */ + gchar *genre; /* genre (utf8) */ + gchar *comment; /* comment (utf8) */ + gchar *composer; /* Composer (utf8) */ + gchar *fdesc; /* eg. "MP3-File"...(utf8)*/ + gchar *grouping; /* ? (utf8) */ + gchar *ipod_path; /* name of file on iPod: uses ":" + instead of "/" */ + guint32 id; /* unique ID of track */ + gint32 size; /* size of file in bytes */ + gint32 tracklen; /* Length of track in ms */ + gint32 cd_nr; /* CD number */ + gint32 cds; /* number of CDs */ + gint32 track_nr; /* track number */ + gint32 tracks; /* number of tracks */ + gint32 bitrate; /* bitrate */ + guint16 samplerate; /* samplerate (CD: 44100) */ + gint32 year; /* year */ + gint32 volume; /* volume adjustment */ + guint32 soundcheck; /* volume adjustment "soundcheck" */ + guint32 time_added; /* time when added (Mac type) */ + guint32 time_played; /* time of last play (Mac type) */ + guint32 time_modified; /* time of last modification (Mac type)*/ + guint32 bookmark_time; /* bookmark set for (AudioBook) in ms */ + guint32 rating; /* star rating (stars * RATING_STEP (20)) */ + guint32 playcount; /* number of times track was played */ + guint32 recent_playcount; /* times track was played since last sync */ + gboolean transferred; /* has file been transferred to iPod? */ + gint16 BPM; /* supposed to vary the playback speed */ + guint8 app_rating; /* star rating set by appl. (not iPod) */ + guint16 type; + guint8 compilation; + guint32 starttime; + guint32 stoptime; + guint8 checked; + guint64 dbid; /* unique database ID */ +/* present in the mhit but not used by gtkpod yet */ + guint32 unk020, unk024, unk084, unk100, unk124; + guint32 unk128, unk132, unk136, unk140, unk144, unk148, unk152; + /* below is for use by application */ + guint64 usertype; + gpointer userdata; + /* function called to duplicate userdata */ + ItdbUserDataDuplicateFunc userdata_duplicate; + /* function called to free userdata */ + ItdbUserDataDestroyFunc userdata_destroy; +} Itdb_Track; +/* (gtkpod note: don't forget to add fields read from the file to + * copy_new_info() in file.c!) */ + +/* Error codes */ +typedef enum +{ + ITDB_FILE_ERROR_SEEK, /* file corrupt: illegal seek occured */ + ITDB_FILE_ERROR_CORRUPT, /* file corrupt */ + ITDB_FILE_ERROR_NOTFOUND, /* file not found */ + ITDB_FILE_ERROR_RENAME, /* file could not be renamed */ + ITDB_FILE_ERROR_ITDB_CORRUPT /* iTunesDB in memory corrupt */ +} ItdbFileError; + +/* Error domain */ +#define ITDB_FILE_ERROR itdb_file_error_quark () +GQuark itdb_file_error_quark (void); + +/* functions for reading/writing database, general itdb functions */ +Itdb_iTunesDB *itdb_parse (const gchar *mp, GError **error); +Itdb_iTunesDB *itdb_parse_file (const gchar *filename, GError **error); +gboolean itdb_write (Itdb_iTunesDB *itdb, const gchar *mp, GError **error); +gboolean itdb_write_file (Itdb_iTunesDB *itdb, const gchar *filename, + GError **error); +gboolean itdb_shuffle_write (Itdb_iTunesDB *itdb, + const gchar *mp, GError **error); +gboolean itdb_shuffle_write_file (Itdb_iTunesDB *itdb, + const gchar *filename, GError **error); +Itdb_iTunesDB *itdb_new (void); +void itdb_free (Itdb_iTunesDB *itdb); +Itdb_iTunesDB *itdb_duplicate (Itdb_iTunesDB *itdb); +guint32 itdb_tracks_number (Itdb_iTunesDB *itdb); +guint32 itdb_tracks_number_nontransferred (Itdb_iTunesDB *itdb); +guint32 itdb_playlists_number (Itdb_iTunesDB *itdb); + +/* general file functions */ +gchar * itdb_resolve_path (const gchar *root, + const gchar * const * components); +gboolean itdb_rename_files (const gchar *mp, GError **error); +gboolean itdb_cp_track_to_ipod (const gchar *mp, Itdb_Track *track, + gchar *filename, GError **error); +gboolean itdb_cp (const gchar *from_file, const gchar *to_file, + GError **error); +void itdb_filename_fs2ipod (gchar *filename); +void itdb_filename_ipod2fs (gchar *ipod_file); +gchar *itdb_filename_on_ipod (const gchar *mp, Itdb_Track *track); + +/* track functions */ +Itdb_Track *itdb_track_new (void); +void itdb_track_free (Itdb_Track *track); +void itdb_track_add (Itdb_iTunesDB *itdb, Itdb_Track *track, gint32 pos); +void itdb_track_remove (Itdb_Track *track); +void itdb_track_unlink (Itdb_Track *track); +Itdb_Track *itdb_track_duplicate (Itdb_Track *tr); +Itdb_Track *itdb_track_by_id (Itdb_iTunesDB *itdb, guint32 id); +GTree *itdb_track_id_tree_create (Itdb_iTunesDB *itdb); +void itdb_track_id_tree_destroy (GTree *idtree); +Itdb_Track *itdb_track_id_tree_by_id (GTree *idtree, guint32 id); + +/* playlist functions */ +Itdb_Playlist *itdb_playlist_new (const gchar *title, gboolean spl); +void itdb_playlist_free (Itdb_Playlist *pl); +void itdb_playlist_add (Itdb_iTunesDB *itdb, Itdb_Playlist *pl, gint32 pos); +void itdb_playlist_move (Itdb_Playlist *pl, guint32 pos); +void itdb_playlist_remove (Itdb_Playlist *pl); +void itdb_playlist_unlink (Itdb_Playlist *pl); +Itdb_Playlist *itdb_playlist_duplicate (Itdb_Playlist *pl); +gboolean itdb_playlist_exists (Itdb_iTunesDB *itdb, Itdb_Playlist *pl); +void itdb_playlist_add_track (Itdb_Playlist *pl, + Itdb_Track *track, gint32 pos); +Itdb_Playlist *itdb_playlist_by_id (Itdb_iTunesDB *itdb, guint64 id); +Itdb_Playlist *itdb_playlist_by_nr (Itdb_iTunesDB *itdb, guint32 num); +Itdb_Playlist *itdb_playlist_by_name (Itdb_iTunesDB *itdb, gchar *name); +Itdb_Playlist *itdb_playlist_mpl (Itdb_iTunesDB *itdb); +gboolean itdb_playlist_contains_track (Itdb_Playlist *pl, Itdb_Track *track); +guint32 itdb_playlist_contain_track_number (Itdb_Track *tr); +void itdb_playlist_remove_track (Itdb_Playlist *pl, Itdb_Track *track); +guint32 itdb_playlist_tracks_number (Itdb_Playlist *pl); +void itdb_playlist_randomize (Itdb_Playlist *pl); + +/* smart playlist functions */ +SPLFieldType itdb_splr_get_field_type (const SPLRule *splr); +SPLActionType itdb_splr_get_action_type (const SPLRule *splr); +void itdb_splr_validate (SPLRule *splr); +void itdb_splr_remove (Itdb_Playlist *pl, SPLRule *splr); +SPLRule *itdb_splr_new (void); +void itdb_splr_add (Itdb_Playlist *pl, SPLRule *splr, gint pos); +SPLRule *itdb_splr_add_new (Itdb_Playlist *pl, gint pos); +void itdb_spl_copy_rules (Itdb_Playlist *dest, Itdb_Playlist *src); +gboolean itdb_splr_eval (Itdb_iTunesDB *itdb, SPLRule *splr, Itdb_Track *track); +void itdb_spl_update (Itdb_iTunesDB *itdb, Itdb_Playlist *spl); +void itdb_spl_update_all (Itdb_iTunesDB *itdb); + +/* time functions */ +guint64 itdb_time_get_mac_time (void); +time_t itdb_time_mac_to_host (guint64 mactime); +guint64 itdb_time_host_to_mac (time_t time); + +#endif diff --git a/src/itdb_itunesdb.c b/src/itdb_itunesdb.c new file mode 100644 index 0000000..f039dee --- /dev/null +++ b/src/itdb_itunesdb.c @@ -0,0 +1,3622 @@ +/* Time-stamp: <2005-08-29 23:23:59 jcs> +| +| Copyright (C) 2002-2005 Jorg Schuler <jcsjcs at users sourceforge net> +| Part of the gtkpod project. +| +| URL: http://www.gtkpod.org/ +| URL: http://gtkpod.sourceforge.net/ +| +| Much of the code in this file has originally been ported from the +| perl script "mktunes.pl" (part of the gnupod-tools collection) +| written by Adrian Ulrich <pab at blinkenlights.ch>. +| +| gnupod-tools: http://www.blinkenlights.ch/cgi-bin/fm.pl?get=ipod +| +| The code contained in this file is free software; you can redistribute +| it and/or modify it under the terms of the GNU Lesser General Public +| License as published by the Free Software Foundation; either version +| 2.1 of the License, or (at your option) any later version. +| +| This file 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 +| Lesser General Public License for more details. +| +| You should have received a copy of the GNU Lesser General Public +| License along with this code; if not, write to the Free Software +| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +| $Id$ +*/ + +/* Some notes on how to use the functions in this file: + + + *** Reading the iTunesDB *** + + Itdb_iTunesDB *itunesdb_parse (gchar *path); /+ path to mountpoint +/ + will read an Itdb_iTunesDB and pass the data over to your program. + + The information given in the "Play Counts" file is also read if + available and the playcounts, star rating and the time last played + is updated. + + Itdb_iTunesDB is a structure containing a GList for the tracks and a + GList for the playlists. + + For each track these fields are set as follows: + + "transferred" will be set to TRUE because all tracks read from a + Itdb_iTunesDB are obviously (or hopefully) already transferred to the + iPod. + + "recent_playcount" is for information only (it will allow to + generate playlists with tracks played since the last time) and will + not be stored to the iPod. + + The master playlist is guaranteed to be the first playlist, and + this must not be changed by your code. + + + *** Writing the Itdb_iTunesDB *** + + gboolean itunesdb_write (gchar *path, Itdb_iTunesDB *itb) + /+ @path to mountpoint, itb to @write +/ + will write an updated version of the Itdb_iTunesDB. + + The "Play Counts" file is renamed to "Play Counts.bak" if it exists + to avoid reading it multiple times. + + Please note that non-transferred tracks are not automatically + transferred to the iPod. A function + + gboolean itunesdb_copy_track_to_ipod (gchar *path, Itdb_Track *track, gchar *pcfile) + + is provided to help you do that, however. + + The following functions most likely will be useful: + + Itdb_Track *itunesdb_new_track (void); + Use itunesdb_new_track() to get an "initialized" track structure + (the "unknowns" are initialized with reasonable values). + + gboolean itunesdb_cp (gchar *from_file, gchar *to_file); + void itunesdb_convert_filename_fs2ipod(gchar *ipod_file); + void itunesdb_convert_filename_ipod2fs(gchar *ipod_file); + + guint32 itunesdb_time_get_mac_time (void); + time_t itunesdb_time_mac_to_host (guint32 mactime); + guint32 itunesdb_time_host_to_mac (time_t time); + + void itunesdb_rename_files (const gchar *dirname); + + (Renames/removes some files on the iPod (Playcounts, OTG + semaphore). Needs to be called if you write the Itdb_iTunesDB not + directly to the iPod but to some other location and then manually + copy the file from there to the iPod. That's much faster in the + case of using an iPod mounted in sync'ed mode.) + + Jorg Schuler, 29.12.2004 */ + + +/* call itdb_parse () to read the Itdb_iTunesDB */ +/* call itdb_write () to write the Itdb_iTunesDB */ + + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <time.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include <stdio.h> +#include "itdb_private.h" +#include <glib/gi18n-lib.h> + +#define ITUNESDB_DEBUG 0 +#define ITUNESDB_MHIT_DEBUG 0 + +#define ITUNESDB_COPYBLK 262144 /* blocksize for cp () */ + + +enum MHOD_ID { + MHOD_ID_TITLE = 1, + MHOD_ID_PATH = 2, + MHOD_ID_ALBUM = 3, + MHOD_ID_ARTIST = 4, + MHOD_ID_GENRE = 5, + MHOD_ID_FDESC = 6, + MHOD_ID_COMMENT = 8, + MHOD_ID_COMPOSER = 12, + MHOD_ID_GROUPING = 13, + MHOD_ID_SPLPREF = 50, /* settings for smart playlist */ + MHOD_ID_SPLRULES = 51, /* rules for smart playlist */ + MHOD_ID_MHYP = 52, /* unknown */ + MHOD_ID_PLAYLIST = 100 +}; + + +/* ID for error domain */ +GQuark itdb_file_error_quark (void) +{ + static GQuark q = 0; + if (q == 0) + q = g_quark_from_static_string ("itdb-file-error-quark"); + return q; +} + +/* Get length of utf16 string in number of characters (words) */ +static guint32 utf16_strlen (gunichar2 *utf16) +{ + guint32 i=0; + if (utf16) + while (utf16[i] != 0) ++i; + return i; +} + + +/* Read the contents of @filename and return a FContents + struct. Returns NULL in case of error and @error is set + accordingly */ +static FContents *fcontents_read (const gchar *fname, GError **error) +{ + FContents *cts; + + g_return_val_if_fail (fname, NULL); + + cts = g_new0 (FContents, 1); + + if (g_file_get_contents (fname, &cts->contents, &cts->length, error)) + { + cts->filename = g_strdup (fname); + } + else + { + g_free (cts); + cts = NULL; + } + return cts; +} + + +/* Frees the memory taken by a FContents structure. NULL pointer will + * be ignored */ +static void fcontents_free (FContents *cts) +{ + if (cts) + { + g_free (cts->filename); + g_free (cts->contents); + /* must not g_error_free (cts->error) because the error was + propagated -> might free the error twice */ + g_free (cts); + } +} + + +/* There seems to be a problem with some distributions (kernel + versions or whatever -- even identical version numbers don't don't + show identical behaviour...): even though vfat is supposed to be + case insensitive, a difference is made between upper and lower case + under some special circumstances. As in "/iPod_Control/Music/F00" + and "/iPod_Control/Music/f00 "... If the former filename does not + exist, we try to find an existing case insensitive match for each + component of the filename. If we can find such a match, we return + it. Otherwise, we return NULL.*/ + +/* We start by assuming that the ipod mount point exists. Then, for + * each component c of track->ipod_path, we try to find an entry d in + * good_path that is case-insensitively equal to c. If we find d, we + * append d to good_path and make the result the new good_path. + * Otherwise, we quit and return NULL. @root: in local encoding, + * @components: in utf8 */ +gchar * itdb_resolve_path (const gchar *root, + const gchar * const * components) +{ + gchar *good_path = g_strdup(root); + guint32 i; + + if (!root) return NULL; + + for(i = 0 ; components[i] ; i++) { + GDir *cur_dir; + gchar *component_as_filename; + gchar *test_path; + gchar *component_stdcase; + const gchar *dir_file=NULL; + + /* skip empty components */ + if (strlen (components[i]) == 0) continue; + component_as_filename = + g_filename_from_utf8(components[i],-1,NULL,NULL,NULL); + test_path = g_build_filename(good_path,component_as_filename,NULL); + g_free(component_as_filename); + if(g_file_test(test_path,G_FILE_TEST_EXISTS)) { + /* This component does not require fixup */ + g_free(good_path); + good_path = test_path; + continue; + } + g_free(test_path); + component_stdcase = g_utf8_casefold(components[i],-1); + /* Case insensitively compare the current component with each entry + * in the current directory. */ + + cur_dir = g_dir_open(good_path,0,NULL); + if (cur_dir) while ((dir_file = g_dir_read_name(cur_dir))) + { + gchar *file_utf8 = g_filename_to_utf8(dir_file,-1,NULL,NULL,NULL); + gchar *file_stdcase = g_utf8_casefold(file_utf8,-1); + gboolean found = !g_utf8_collate(file_stdcase,component_stdcase); + gchar *new_good_path; + g_free(file_stdcase); + if(!found) + { + /* This is not the matching entry */ + g_free(file_utf8); + continue; + } + + new_good_path = dir_file ? g_build_filename(good_path,dir_file,NULL) : NULL; + g_free(good_path); + good_path= new_good_path; + /* This is the matching entry, so we can stop searching */ + break; + } + + if(!dir_file) { + /* We never found a matching entry */ + g_free(good_path); + good_path = NULL; + } + + g_free(component_stdcase); + if (cur_dir) g_dir_close(cur_dir); + if(!good_path || !g_file_test(good_path,G_FILE_TEST_EXISTS)) + break; /* We couldn't fix this component, so don't try later ones */ + } + + if(good_path && g_file_test(good_path,G_FILE_TEST_EXISTS)) + return good_path; + + return NULL; +} + + +/* Check if the @seek with length @len is legal or out of + * range. Returns TRUE if legal and FALSE when it is out of range, in + * which case cts->error is set as well. */ +static gboolean check_seek (FContents *cts, glong seek, glong len) +{ + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (cts->contents, FALSE); + + if ((seek+len <= cts->length) && (seek >=0)) + { + return TRUE; + } + else + { + g_return_val_if_fail (cts->filename, FALSE); + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_SEEK, + _("Illegal seek to offset %ld (length %ld) in file '%s'."), + seek, len, cts->filename); + return FALSE; + } +} + + +/* Copies @len bytes from position @seek in @cts->contents to + @data. Returns FALSE on error and sets cts->error accordingly. */ +static gboolean seek_get_n_bytes (FContents *cts, gchar *data, + glong seek, glong len) +{ + if (check_seek (cts, seek, len)) + { + memcpy (data, &cts->contents[seek], len); + return TRUE; + } + return FALSE; +} + +/* Compare @n bytes of @cts->contents starting at @seek and + * @data. Returns TRUE if equal, FALSE if not. Also returns FALSE on + * error, so you must check cts->error */ +static gboolean cmp_n_bytes_seek (FContents *cts, gchar *data, + glong seek, glong len) +{ + if (check_seek (cts, seek, len)) + { + gint i; + for (i=0; i<len; ++i) + { + if (cts->contents[seek+i] != data[i]) return FALSE; + } + return TRUE; + } + return FALSE; +} + + +/* Returns the 1-byte number stored at position @seek. On error the + * GError in @cts is set. */ +static guint8 get8int (FContents *cts, glong seek) +{ + guint8 n=0; + + if (check_seek (cts, seek, 1)) + { + n = cts->contents[seek]; + } + return n; +} + + +/* Get the 4-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint32 get32lint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 4)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 4); +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + } + return n; +} + + +/* Get the 4-byte-number stored at position "seek" in big endian + encoding. On error the GError in @cts is set. */ +static guint32 get32bint (FContents *cts, glong seek) +{ + guint32 n=0; + + if (check_seek (cts, seek, 4)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 4); +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + } + return n; +} + +/* Get the 8-byte-number stored at position "seek" in little endian + encoding. On error the GError in @cts is set. */ +static guint64 get64lint (FContents *cts, glong seek) +{ + guint64 n=0; + + if (check_seek (cts, seek, 8)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 8); +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + } + return n; +} + + +/* Get the 8-byte-number stored at position "seek" in big endian + encoding. On error the GError in @cts is set. */ +static guint64 get64bint (FContents *cts, glong seek) +{ + guint64 n=0; + + if (check_seek (cts, seek, 8)) + { + g_return_val_if_fail (cts->contents, 0); + memcpy (&n, &cts->contents[seek], 8); +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + } + return n; +} + +/* Fix little endian UTF16 String to correct byteorder if necessary + * (all strings in the Itdb_iTunesDB are little endian except for the ones + * in smart playlists). */ +static gunichar2 *fixup_little_utf16 (gunichar2 *utf16_string) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + gint32 i; + if (utf16_string) + { + for(i=0; i<utf16_strlen(utf16_string); i++) + { + utf16_string[i] = GUINT16_SWAP_LE_BE (utf16_string[i]); + } + } +# endif + return utf16_string; +} + +/* Fix big endian UTF16 String to correct byteorder if necessary (only + * strings in smart playlists are big endian) */ +static gunichar2 *fixup_big_utf16 (gunichar2 *utf16_string) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + gint32 i; + if (utf16_string) + { + for(i=0; i<utf16_strlen(utf16_string); i++) + { + utf16_string[i] = GUINT16_SWAP_LE_BE (utf16_string[i]); + } + } +# endif + return utf16_string; +} + + +#define CHECK_ERROR(imp, val) if (cts->error) { g_propagate_error (&imp->error, cts->error); return (val); } + + +/* get next playcount, that is the first entry of GList + * playcounts. This entry is removed from the list. You must free the + * return value after use */ +static struct playcount *playcount_get_next (FImport *fimp) +{ + struct playcount *playcount; + g_return_val_if_fail (fimp, NULL); + + playcount = g_list_nth_data (fimp->playcounts, 0); + + if (playcount) + fimp->playcounts = g_list_remove (fimp->playcounts, playcount); + return playcount; +} + +/* delete all entries of GList *playcounts */ +static void playcounts_free (FImport *fimp) +{ + struct playcount *playcount; + + g_return_if_fail (fimp); + + while ((playcount=playcount_get_next (fimp))) g_free (playcount); +} + + +/* called by init_playcounts */ +static gboolean playcounts_read (FImport *fimp, FContents *cts) +{ + guint32 header_length, entry_length, entry_num, i=0; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (cts, FALSE); + + if (!cmp_n_bytes_seek (cts, "mhdp", 0, 4)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + } + else + { /* set error */ + g_return_val_if_fail (cts->filename, FALSE); + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a Play Counts file: '%s' (missing mhdp header)."), + cts->filename); + } + return FALSE; + } + header_length = get32lint (cts, 4); + CHECK_ERROR (fimp, FALSE); + /* all the headers I know are 0x60 long -- if this one is longer + we can simply ignore the additional information */ + if (header_length < 0x60) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Play Counts file ('%s'): header length smaller than expected (%d<96)."), + cts->filename, header_length); + return FALSE; + } + entry_length = get32lint (cts, 8); + CHECK_ERROR (fimp, FALSE); + /* all the entries I know are 0x0c (firmware 1.3) or 0x10 + * (firmware 2.0) or 0x14 (iTunesDB version 0x0d) in length */ + if (entry_length < 0x0c) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Play Counts file ('%s'): entry length smaller than expected (%d<12)."), + cts->filename, entry_length); + return FALSE; + } + /* number of entries */ + entry_num = get32lint (cts, 12); + CHECK_ERROR (fimp, FALSE); + for (i=0; i<entry_num; ++i) + { + struct playcount *playcount = g_new0 (struct playcount, 1); + glong seek = header_length + i*entry_length; + + fimp->playcounts = g_list_append (fimp->playcounts, playcount); + playcount->playcount = get32lint (cts, seek); + CHECK_ERROR (fimp, FALSE); + playcount->time_played = get32lint (cts, seek+4); + CHECK_ERROR (fimp, FALSE); + playcount->bookmark_time = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + /* NOTE: + * + * The iPod (firmware 1.3, 2.0, ...?) doesn't seem to use the + * timezone information correctly -- no matter what you set + * iPod's timezone to, it will always record as if it were set + * to UTC -- we need to subtract the difference between + * current timezone and UTC to get a correct + * display. -- this should be done by the application were + * necessary */ + + /* rating only exists if the entry length is at least 0x10 */ + if (entry_length >= 0x10) + { + playcount->rating = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + } + else + { + playcount->rating = NO_PLAYCOUNT; + } + /* unk16 only exists if the entry length is at least 0x14 */ + if (entry_length >= 0x14) + { + playcount->unk16 = get32lint (cts, seek+16); + CHECK_ERROR (fimp, FALSE); + } + else + { + playcount->unk16 = 0; + } + } + return TRUE; +} + + + +/* Read the Play Count file (formed by adding "Play Counts" to the + * directory component of fimp->itdb->itdb_filename) and set up the + * GList *playcounts. + * Returns TRUE on success (also when no Play Count + * file is found as this is not an error) and FALSE otherwise, in + * which case fimp->error is set accordingly. */ +static gboolean playcounts_init (FImport *fimp) +{ + const gchar *db[] = {"Play Counts", NULL}; + gchar *plcname, *dirname; + gboolean result=FALSE; + struct stat filestat; + FContents *cts; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (!fimp->error, FALSE); + g_return_val_if_fail (!fimp->playcounts, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itdb->filename, FALSE); + + dirname = g_path_get_dirname (fimp->itdb->filename); + + plcname = itdb_resolve_path (dirname, db); + + g_free (dirname); + + /* skip if no playcounts file is present */ + if (!plcname) return TRUE; + + /* skip if playcounts file has zero-length (often happens after + * dosfsck) */ + stat (plcname, &filestat); + if (filestat.st_size < 0x60) return TRUE; /* check for header length */ + + cts = fcontents_read (plcname, &fimp->error); + if (cts) + { + result = playcounts_read (fimp, cts); + fcontents_free (cts); + } + g_free (plcname); + return result; +} + + +/* Free the memory taken by @fimp. fimp->itdb must be freed separately + * before calling this function */ +static void itdb_free_fimp (FImport *fimp) +{ + if (fimp) + { + if (fimp->itunesdb) fcontents_free (fimp->itunesdb); + g_list_free (fimp->pos_glist); + playcounts_free (fimp); + g_free (fimp); + } +} + +/* Free the memory taken by @itdb. */ +void itdb_free (Itdb_iTunesDB *itdb) +{ + if (itdb) + { + g_list_foreach (itdb->playlists, + (GFunc)(itdb_playlist_free), NULL); + g_list_free (itdb->playlists); + g_list_foreach (itdb->tracks, + (GFunc)(itdb_track_free), NULL); + g_list_free (itdb->tracks); + g_free (itdb->filename); + g_free (itdb->mountpoint); + if (itdb->userdata && itdb->userdata_destroy) + (*itdb->userdata_destroy) (itdb->userdata); + g_free (itdb); + } +} + +/* Free the memory taken by @itdb. */ +Itdb_iTunesDB *itdb_duplicate (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, NULL); + g_return_val_if_fail (!itdb->userdata || + itdb->userdata_duplicate, NULL); + /* FIXME: not yet implemented */ + g_return_val_if_reached (NULL); +} + +/* return number of playlists */ +guint32 itdb_playlists_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + + return g_list_length (itdb->playlists); +} + + +/* return total number of tracks */ +guint32 itdb_tracks_number (Itdb_iTunesDB *itdb) +{ + g_return_val_if_fail (itdb, 0); + + return g_list_length (itdb->tracks); +} + + +guint32 itdb_tracks_number_nontransferred (Itdb_iTunesDB *itdb) +{ + guint n = 0; + GList *gl; + g_return_val_if_fail (itdb, 0); + + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + g_return_val_if_fail (track, 0); + if (!track->transferred) ++n; + } + return n; +} + + + +/* Creates a new Itdb_iTunesDB with the unknowns filled in to reasonable + values */ +Itdb_iTunesDB *itdb_new (void) +{ + GRand *grand = g_rand_new (); + + Itdb_iTunesDB *itdb = g_new0 (Itdb_iTunesDB, 1); + itdb->version = 0x09; + itdb->id = ((guint64)g_rand_int (grand) << 32) | + ((guint64)g_rand_int (grand)); + return itdb; +} + +/* Returns the type of the mhod and the length *ml. *ml is set to -1 + * on error (e.g. because there's no mhod at @seek). */ +/* A return value of -1 and no error set means that no mhod was found + at @seek */ +static gint32 get_mhod_type (FContents *cts, glong seek, gint32 *ml) +{ + gint32 type = -1; + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhod_type seek: %x\n", (int)seek); +#endif + + if (ml) *ml = -1; + + if (cmp_n_bytes_seek (cts, "mhod", seek, 4)) + { + guint32 len = get32lint (cts, seek+8); /* total length */ + if (cts->error) return -1; + if (ml) *ml = len; + type = get32lint (cts, seek+12); /* mhod_id */ + if (cts->error) return -1; + } + return type; +} + +/* Returns a pointer to the data contained in the mhod at position + @seek. This can be a simple string or something more complicated as + in the case for SPLPREF or SPLRULES. *ml is set to the total length + of the mhod (-1 in case of an error), *mty is set to the type of + the mhod. + On error NULL is returned and cts->error is set appropriately. */ +static void *get_mhod (FContents *cts, gulong mhod_seek, + gint32 *ml, gint32 *mty) +{ + gunichar2 *entry_utf16 = NULL; + SPLPref *splp = NULL; + guint8 limitsort_opposite; + void *result = NULL; + gint32 xl, len; + gint32 header_length; + gulong seek; + + g_return_val_if_fail (ml, NULL); + g_return_val_if_fail (mty, NULL); + g_return_val_if_fail (cts, NULL); + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhod seek: %ld\n", mhod_seek); +#endif + + g_return_val_if_fail (cts, NULL); + + *ml = -1; + + g_return_val_if_fail (!cts->error, NULL); + + *mty = get_mhod_type (cts, mhod_seek, &len); + if (*mty == -1) + { + if (!cts->error) + { /* set error */ + g_set_error (&cts->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: no MHOD at offset %ld in file '%s'."), + mhod_seek, cts->filename); + } + return NULL; + } + header_length = get32lint (cts, mhod_seek+4); /* header length */ + if (cts->error) return NULL; + + seek = mhod_seek + header_length; + +#if ITUNESDB_DEBUG + fprintf(stderr, "ml: %x mty: %x\n", *ml, *mty); +#endif + + switch ((enum MHOD_ID)*mty) + { + case MHOD_ID_MHYP: + /* this is not yet supported */ + case MHOD_ID_PLAYLIST: + /* return the position indicator */ + result = (void *)get32lint (cts, mhod_seek+24); + if (cts->error) return NULL; + break; + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + xl = get32lint (cts, seek+4); /* entry length */ + if (cts->error) return NULL; + entry_utf16 = g_new0 (gunichar2, (xl+2)/2); + if (seek_get_n_bytes (cts, (gchar *)entry_utf16, seek+16, xl)) + { + result = fixup_little_utf16 (entry_utf16); + } + else + { /* error */ + g_free (entry_utf16); + return NULL; + } + break; + case MHOD_ID_SPLPREF: /* Settings for smart playlist */ + splp = g_new0 (SPLPref, 1); + splp->liveupdate = get8int (cts, seek); + if (cts->error) return NULL; + splp->checkrules = get8int (cts, seek+1); + if (cts->error) return NULL; + splp->checklimits = get8int (cts, seek+2); + if (cts->error) return NULL; + splp->limittype = get8int (cts, seek+3); + if (cts->error) return NULL; + splp->limitsort = get8int (cts, seek+4); + if (cts->error) return NULL; + splp->limitvalue = get32lint (cts, seek+8); + if (cts->error) return NULL; + splp->matchcheckedonly = get8int (cts, seek+12); + if (cts->error) return NULL; + limitsort_opposite = get8int (cts, seek+13); + if (cts->error) return NULL; + /* if the opposite flag is on, set limitsort's high bit -- see + note in itunesdb.h for more info */ + if (limitsort_opposite) + splp->limitsort |= 0x80000000; + result = splp; + break; + case MHOD_ID_SPLRULES: /* Rules for smart playlist */ + if (cmp_n_bytes_seek (cts, "SLst", seek, 4)) + { + /* !!! for some reason the SLst part is the only part of the + iTunesDB with big-endian encoding, including UTF16 + strings */ + gint i; + guint32 numrules; + SPLRules *splrs = g_new0 (SPLRules, 1); + splrs->unk004 = get32bint (cts, seek+4); + if (cts->error) return NULL; + numrules = get32bint (cts, seek+8); + if (cts->error) return NULL; + splrs->match_operator = get32bint (cts, seek+12); + if (cts->error) return NULL; + seek += 136; /* I can't find this value stored in the + iTunesDB :-( */ + for (i=0; i<numrules; ++i) + { + guint32 length; + SPLRule *splr = g_new0 (SPLRule, 1); + splr->field = get32bint (cts, seek); + if (cts->error) return NULL; + splr->action = get32bint (cts, seek+4); + if (cts->error) return NULL; + seek += 52; + length = get32bint (cts, seek); + if (cts->error) return NULL; + if (itdb_spl_action_known (splr->action)) + { + gint ft = itdb_splr_get_field_type (splr); + if (ft == splft_string) + { + gunichar2 *string_utf16 = g_new0 (gunichar2, + (length+2)/2); + if (!seek_get_n_bytes (cts, (gchar *)string_utf16, + seek+4, length)) + { + g_free (string_utf16); + g_free (splr); + return NULL; + } + fixup_big_utf16 (string_utf16); + splr->string = g_utf16_to_utf8 ( + string_utf16, -1, NULL, NULL, NULL); + g_free (string_utf16); + } + else + { + if (length != 0x44) + { + g_warning (_("Length of smart playlist rule field (%d) not as expected. Trying to continue anyhow.\n"), length); + } + splr->fromvalue = get64bint (cts, seek+4); + if (cts->error) return NULL; + splr->fromdate = get64bint (cts, seek+12); + if (cts->error) return NULL; + splr->fromunits = get64bint (cts, seek+20); + if (cts->error) return NULL; + splr->tovalue = get64bint (cts, seek+28); + if (cts->error) return NULL; + splr->todate = get64bint (cts, seek+36); + if (cts->error) return NULL; + splr->tounits = get64bint (cts, seek+44); + if (cts->error) return NULL; + /* SPLFIELD_PLAYLIST seem to use these unknowns*/ + splr->unk052 = get32bint (cts, seek+52); + if (cts->error) return NULL; + splr->unk056 = get32bint (cts, seek+56); + if (cts->error) return NULL; + splr->unk060 = get32bint (cts, seek+60); + if (cts->error) return NULL; + splr->unk064 = get32bint (cts, seek+64); + if (cts->error) return NULL; + splr->unk068 = get32bint (cts, seek+68); + if (cts->error) return NULL; + } + seek += length+4; + } + else + { + g_free (splr); + splr = NULL; + } + if (splr) + { + splrs->rules = g_list_append (splrs->rules, splr); + } + } + result = splrs; + } + else + { + if (!cts->error) + g_warning (_("Did not find SLst hunk as expected. Trying to continue.\n")); + else + return NULL; + } + break; + default: + g_warning (_("Encountered unknown MHOD type (%d) while parsing the iTunesDB. Ignoring.\n\n"), *mty); + break; + } + *ml = len; + return result; +} + +/* Returns the value of a string type mhod. return the length of the + mhod *ml, the mhod type *mty, and a string with the entry (in + UTF16). After use you must free the string with g_free(). Returns + NULL if no string is avaible. *ml is set to -1 in case of error and + cts->error is set appropriately. */ +static gunichar2 *get_mhod_string (FContents *cts, glong seek, gint32 *ml, gint32 *mty) +{ + gunichar2 *result = NULL; + + *mty = get_mhod_type (cts, seek, ml); + if (cts->error) return NULL; + + if (*ml != -1) switch ((enum MHOD_ID)*mty) + { + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + result = get_mhod (cts, seek, ml, mty); + break; + case MHOD_ID_SPLPREF: + case MHOD_ID_SPLRULES: + case MHOD_ID_MHYP: + case MHOD_ID_PLAYLIST: + /* these do not have a string entry */ + break; + } + return result; +} + + +/* Get a playlist. Returns the position where the next playlist should + be. On error -1 is returned and fimp->error is set appropriately. */ +static glong get_playlist (FImport *fimp, glong seek) +{ + gint pos_comp (gpointer a, gpointer b) + { + return ((gint)a - (gint)b); + } + + gunichar2 *plname_utf16 = NULL; + guint32 i, type, tracknum, mhod_num; + glong nextseek; + guint32 hlen; + Itdb_Playlist *plitem = NULL; + FContents *cts; + +#if ITUNESDB_DEBUG + fprintf(stderr, "mhyp seek: %x\n", (int)seek); +#endif + g_return_val_if_fail (fimp, -1); + g_return_val_if_fail (fimp->idtree, -1); + + cts = fimp->itunesdb; + + if (!cmp_n_bytes_seek (cts, "mhyp", seek, 4)) + { + if (cts->error) + g_propagate_error (&fimp->error, cts->error); + return -1; + } + hlen = get32lint (cts, seek+4); /* length of header */ + CHECK_ERROR (fimp, -1); + if (hlen == 0) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, cts->filename); + return -1; + } + nextseek = seek + get32lint (cts, seek+8);/* possible begin of next PL */ + CHECK_ERROR (fimp, -1); + mhod_num = get32lint (cts, seek+12); /* number of MHODs we expect */ + CHECK_ERROR (fimp, -1); + tracknum = get32lint (cts, seek+16); /* number of tracks in playlist */ + CHECK_ERROR (fimp, -1); + plitem = itdb_playlist_new (NULL, FALSE); + /* Some Playlists have added 256 to their type -- I don't know what + it's for, so we just ignore it for now -> & 0xff */ + plitem->type = get32lint (cts, seek+20) & 0xff; + CHECK_ERROR (fimp, -1); + plitem->id = get64lint (cts, seek+28); + CHECK_ERROR (fimp, -1); + plitem->unk036 = get32lint (cts, seek+36); + CHECK_ERROR (fimp, -1); + plitem->unk040 = get32lint (cts, seek+40); + CHECK_ERROR (fimp, -1); + plitem->unk044 = get32lint (cts, seek+44); + CHECK_ERROR (fimp, -1); + for (i=0; i < mhod_num; ++i) + { + gunichar2 *plname_utf16_maybe; + SPLPref *splpref = NULL; + SPLRules *splrules = NULL; + + seek += hlen; + type = get_mhod_type (cts, seek, &hlen); + CHECK_ERROR (fimp, -1); + if (hlen != -1) switch ((enum MHOD_ID)type) + { + case MHOD_ID_PLAYLIST: + /* here we could do something about the playlist settings */ + break; + case MHOD_ID_TITLE: + plname_utf16_maybe = get_mhod (cts, seek, &hlen, &type); + CHECK_ERROR (fimp, -1); + if (plname_utf16_maybe) + { + /* sometimes there seem to be two mhod TITLE headers */ + g_free (plname_utf16); + plname_utf16 = plname_utf16_maybe; + } + break; + case MHOD_ID_SPLPREF: + splpref = get_mhod (cts, seek, &hlen, &type); + CHECK_ERROR (fimp, -1); + if (splpref) + { + plitem->is_spl = TRUE; + memcpy (&plitem->splpref, splpref, sizeof (SPLPref)); + g_free (splpref); + splpref = NULL; + } + break; + case MHOD_ID_SPLRULES: + splrules = get_mhod (cts, seek, &hlen, &type); + CHECK_ERROR (fimp, -1); + if (splrules) + { + plitem->is_spl = TRUE; + memcpy (&plitem->splrules, splrules, sizeof (SPLRules)); + g_free (splrules); + splrules = NULL; + } + break; + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + /* these are not expected here */ + break; + case MHOD_ID_MHYP: + /* this I don't know how to handle */ + break; + } + } + + if (plname_utf16) + { + plitem->name = g_utf16_to_utf8 (plname_utf16, -1, NULL, NULL, NULL); + g_free (plname_utf16); + } + else + { /* we did not read a valid mhod TITLE header -> */ + /* we simply make up our own name */ + if (plitem->type == ITDB_PL_TYPE_MPL) + plitem->name = _("Master-PL"); + else + plitem->name = _("Playlist"); + } + +#if ITUNESDB_DEBUG + fprintf(stderr, "pln: %s(%d Itdb_Tracks) \n", plitem->name, (int)tracknum); +#endif + + /* add new playlist */ + itdb_playlist_add (fimp->itdb, plitem, -1); + + i=0; /* tracks read */ + while (i<tracknum) + { + guint32 len = get32lint (cts, seek+8); +#if ITUNESDB_DEBUG + fprintf(stderr, " %lx: seeking track %d of %d\n", seek, i+1, (int)tracknum); +#endif + CHECK_ERROR (fimp, -1); + /* We read the mhip headers and skip everything else (the mhips + * seem to come in pairs: mhip/mhod mhip/mhod ...). */ + if (cmp_n_bytes_seek (cts, "mhyp", seek, 4)) + { /* This cannot be, let's abort... */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: found mhyp at %ld in file '%s'."), + seek, cts->filename); + return -1; + } + if (cmp_n_bytes_seek (cts, "mhip", seek, 4)) + { +#if ITUNESDB_DEBUG + fprintf(stderr, " %lx: mhit\n", seek); +#endif + Itdb_Track *tr; + gint32 pos = -1; + guint32 posid; + gint32 mhod_type; + gint32 mhod_len; + guint32 mhit_len; + guint32 ref; + mhit_len = get32lint(cts, seek+4); + CHECK_ERROR (fimp, -1); + ref = get32lint(cts, seek+24); + CHECK_ERROR (fimp, -1); + /* the mhod that follows gives us the position in the + playlist (type 100) */ + mhod_type = get_mhod_type (cts, seek+mhit_len, NULL); + CHECK_ERROR (fimp, -1); + if (mhod_type == MHOD_ID_PLAYLIST) + { + posid = (guint32)get_mhod (cts, seek+mhit_len, + &mhod_len, &mhod_type); + CHECK_ERROR (fimp, -1); + /* The posids don't have to be in numeric order, but our + database depends on the playlist members being sorted + according to the order they appear in the + playlist. Therefore we need to find out at which + position to insert the track */ + fimp->pos_glist = g_list_insert_sorted ( + fimp->pos_glist, (gpointer)posid, + (GCompareFunc)pos_comp); + pos = g_list_index (fimp->pos_glist, (gpointer)posid); + /* For performance reasons set pos to -1 if position is + end of list */ + if (pos == i) pos = -1; + } + tr = itdb_track_id_tree_by_id (fimp->idtree, ref); + if (tr) + { + itdb_playlist_add_track (plitem, tr, pos); + } + else + g_warning (_("Itdb_Track ID '%d' not found.\n"), ref); + ++i; + } + CHECK_ERROR (fimp, -1); + seek += len; + } + g_list_free (fimp->pos_glist); + fimp->pos_glist = NULL; + return nextseek; +} + + +/* returns a pointer to the next header or -1 on error. fimp->error is + set appropriately. If no "mhit" header is found at the location + specified, -1 is returned but no error is set. */ +static glong get_mhit (FImport *fimp, glong seek) +{ + Itdb_Track *track; + gchar *entry_utf8; + gunichar2 *entry_utf16; + gint32 type, zip; + struct playcount *playcount; + guint32 i, temp, mhod_nums; + FContents *cts; + +#if ITUNESDB_DEBUG + fprintf(stderr, "get_mhit seek: %x\n", (int)seek); +#endif + + g_return_val_if_fail (fimp, -1); + + cts = fimp->itunesdb; + + if (!cmp_n_bytes_seek (cts, "mhit", seek, 4)) + { + if (cts->error) + g_propagate_error (&fimp->error, cts->error); + return -1; + } + + mhod_nums = get32lint (cts, seek+12); + CHECK_ERROR (fimp, -1); + + track = itdb_track_new (); + + track->id = get32lint(cts, seek+16); /* iPod ID */ + CHECK_ERROR (fimp, -1); + track->unk020 = get32lint (cts, seek+20); + CHECK_ERROR (fimp, -1); + track->unk024 = get32lint (cts, seek+24); + CHECK_ERROR (fimp, -1); + temp = get32lint (cts, seek+28); + CHECK_ERROR (fimp, -1); + track->rating = (temp & 0xff000000) >> 24; /* rating */ + track->compilation = (temp & 0x00ff0000) >> 16; + track->type = temp & 0x0000ffff; + track->time_added = get32lint(cts, seek+32); /* time added */ + CHECK_ERROR (fimp, -1); + track->size = get32lint(cts, seek+36); /* file size */ + CHECK_ERROR (fimp, -1); + track->tracklen = get32lint(cts, seek+40); /* time */ + CHECK_ERROR (fimp, -1); + track->track_nr = get32lint(cts, seek+44); /* track number */ + CHECK_ERROR (fimp, -1); + track->tracks = get32lint(cts, seek+48); /* nr of tracks */ + CHECK_ERROR (fimp, -1); + track->year = get32lint(cts, seek+52); /* year */ + CHECK_ERROR (fimp, -1); + track->bitrate = get32lint(cts, seek+56); /* bitrate */ + CHECK_ERROR (fimp, -1); + track->samplerate = get32lint(cts,seek+60)>>16; /* sample rate */ + CHECK_ERROR (fimp, -1); + track->volume = get32lint(cts, seek+64); /* volume adjust */ + CHECK_ERROR (fimp, -1); + track->starttime = get32lint (cts, seek+68); + CHECK_ERROR (fimp, -1); + track->stoptime = get32lint (cts, seek+72); + CHECK_ERROR (fimp, -1); + track->soundcheck = get32lint (cts, seek+76); /* soundcheck */ + CHECK_ERROR (fimp, -1); + track->playcount = get32lint (cts, seek+80); /* playcount */ + CHECK_ERROR (fimp, -1); + track->unk084 = get32lint (cts, seek+84); + CHECK_ERROR (fimp, -1); + track->time_played = get32lint(cts, seek+88); /* last time played */ + CHECK_ERROR (fimp, -1); + track->cd_nr = get32lint(cts, seek+92); /* CD nr */ + CHECK_ERROR (fimp, -1); + track->cds = get32lint(cts, seek+96); /* CD nr of.. */ + CHECK_ERROR (fimp, -1); + track->unk100 = get32lint (cts, seek+100); + CHECK_ERROR (fimp, -1); + track->time_modified = get32lint(cts, seek+104);/* last mod. time */ + CHECK_ERROR (fimp, -1); + track->bookmark_time = get32lint (cts, seek+108); /* time bookmarked */ + CHECK_ERROR (fimp, -1); + track->dbid = get64lint (cts, seek+112); + CHECK_ERROR (fimp, -1); + temp = get32lint (cts, seek+120); + CHECK_ERROR (fimp, -1); + track->BPM = temp >> 16; + track->app_rating = (temp & 0xff00)>> 8;/* The rating set by * the + application, as opposed to + the rating set on the iPod + itself */ + track->checked = temp & 0xff; /* Checked/Unchecked: 0/1 */ + track->unk124 = get32lint (cts, seek+124); + CHECK_ERROR (fimp, -1); + track->unk128 = get32lint (cts, seek+128); + CHECK_ERROR (fimp, -1); + track->unk132 = get32lint (cts, seek+132); + CHECK_ERROR (fimp, -1); + track->unk136 = get32lint (cts, seek+136); + CHECK_ERROR (fimp, -1); + track->unk140 = get32lint (cts, seek+140); + CHECK_ERROR (fimp, -1); + track->unk144 = get32lint (cts, seek+144); + CHECK_ERROR (fimp, -1); + track->unk148 = get32lint (cts, seek+148); + CHECK_ERROR (fimp, -1); + track->unk152 = get32lint (cts, seek+152); + CHECK_ERROR (fimp, -1); + + track->transferred = TRUE; /* track is on iPod! */ + +#if ITUNESDB_MHIT_DEBUG +time_t time_mac_to_host (guint32 mactime); +gchar *time_time_to_string (time_t time); +#define printf_mhit(sk, str) printf ("%3d: %d (%s)\n", sk, get32lint (file, seek+sk), str); +#define printf_mhit_time(sk, str) { gchar *buf = time_time_to_string (itunesdb_time_mac_to_host (get32lint (file, seek+sk))); printf ("%3d: %s (%s)\n", sk, buf, str); g_free (buf); } + { + printf ("\nmhit: seek=%lu\n", seek); + printf_mhit ( 4, "header size"); + printf_mhit ( 8, "mhit size"); + printf_mhit ( 12, "nr of mhods"); + printf_mhit ( 16, "iPod ID"); + printf_mhit ( 20, "?"); + printf_mhit ( 24, "?"); + printf (" 28: %u (type)\n", get32lint (file, seek+28) & 0xffffff); + printf (" 28: %u (rating)\n", get32lint (file, seek+28) >> 24); + printf_mhit ( 32, "timestamp file"); + printf_mhit_time ( 32, "timestamp file"); + printf_mhit ( 36, "size"); + printf_mhit ( 40, "tracklen (ms)"); + printf_mhit ( 44, "track_nr"); + printf_mhit ( 48, "total tracks"); + printf_mhit ( 52, "year"); + printf_mhit ( 56, "bitrate"); + printf_mhit ( 60, "sample rate"); + printf (" 60: %u (sample rate LSB)\n", get32lint (file, seek+60) & 0xffff); + printf (" 60: %u (sample rate HSB)\n", (get32lint (file, seek+60) >> 16)); + printf_mhit ( 64, "?"); + printf_mhit ( 68, "?"); + printf_mhit ( 72, "?"); + printf_mhit ( 76, "?"); + printf_mhit ( 80, "playcount"); + printf_mhit ( 84, "?"); + printf_mhit ( 88, "last played"); + printf_mhit_time ( 88, "last played"); + printf_mhit ( 92, "CD"); + printf_mhit ( 96, "total CDs"); + printf_mhit (100, "?"); + printf_mhit (104, "?"); + printf_mhit_time (104, "?"); + printf_mhit (108, "?"); + printf_mhit (112, "?"); + printf_mhit (116, "?"); + printf_mhit (120, "?"); + printf_mhit (124, "?"); + printf_mhit (128, "?"); + printf_mhit (132, "?"); + printf_mhit (136, "?"); + printf_mhit (140, "?"); + printf_mhit (144, "?"); + printf_mhit (148, "?"); + printf_mhit (152, "?"); + } +#undef printf_mhit_time +#undef printf_mhit +#endif + + seek += get32lint (cts, seek+4); /* 1st mhod starts here! */ + CHECK_ERROR (fimp, -1); + + for (i=0; i<mhod_nums; ++i) + { + entry_utf16 = get_mhod_string (cts, seek, &zip, &type); + CHECK_ERROR (fimp, -1); + if (entry_utf16 != NULL) + { + entry_utf8 = g_utf16_to_utf8 (entry_utf16, -1, NULL, NULL, NULL); + switch ((enum MHOD_ID)type) + { + case MHOD_ID_ALBUM: + track->album = entry_utf8; + break; + case MHOD_ID_ARTIST: + track->artist = entry_utf8; + break; + case MHOD_ID_TITLE: + track->title = entry_utf8; + break; + case MHOD_ID_GENRE: + track->genre = entry_utf8; + break; + case MHOD_ID_PATH: + track->ipod_path = entry_utf8; + break; + case MHOD_ID_FDESC: + track->fdesc = entry_utf8; + break; + case MHOD_ID_COMMENT: + track->comment = entry_utf8; + break; + case MHOD_ID_COMPOSER: + track->composer = entry_utf8; + break; + case MHOD_ID_GROUPING: + track->grouping = entry_utf8; + break; + default: /* unknown entry -- discard */ + g_free (entry_utf8); + break; + } + g_free (entry_utf16); + } + seek += zip; + } + + playcount = playcount_get_next (fimp); + if (playcount) + { + if (playcount->rating != NO_PLAYCOUNT) + track->rating = playcount->rating; + + if (playcount->time_played) + track->time_played = playcount->time_played; + + if (playcount->bookmark_time) + track->bookmark_time = playcount->bookmark_time; + + track->playcount += playcount->playcount; + track->recent_playcount = playcount->playcount; + g_free (playcount); + } + itdb_track_add (fimp->itdb, track, -1); + return seek; +} + + +/* Called by read_OTG_playlists(): OTG playlist stored in @cts by + * adding a new playlist (named @plname) with the tracks specified in + * @cts. If @plname is NULL, a standard name will be substituted */ +/* Returns FALSE on error, TRUE on success. On error @fimp->error will + * be set apropriately. */ +static gboolean process_OTG_file (FImport *fimp, FContents *cts, + const gchar *plname) +{ + guint32 header_length, entry_length, entry_num; + + g_return_val_if_fail (fimp && cts, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + + if (!plname) plname = _("OTG Playlist"); + + if (!cmp_n_bytes_seek (cts, "mhpo", 0, 4)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + } + else + { /* set error */ + g_return_val_if_fail (cts->filename, FALSE); + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a OTG playlist file: '%s' (missing mhpo header)."), + cts->filename); + } + return FALSE; + } + header_length = get32lint (cts, 4); + CHECK_ERROR (fimp, FALSE); + /* all the headers I know are 0x14 long -- if this one is + longer we can simply ignore the additional information */ + if (header_length < 0x14) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("OTG playlist file ('%s'): header length smaller than expected (%d<20)."), + cts->filename, header_length); + return FALSE; + } + entry_length = get32lint (cts, 8); + CHECK_ERROR (fimp, FALSE); + /* all the entries I know are 0x04 long */ + if (entry_length < 0x04) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("OTG playlist file file ('%s'): entry length smaller than expected (%d<4)."), + cts->filename, entry_length); + return FALSE; + } + /* number of entries */ + entry_num = get32lint (cts, 12); + CHECK_ERROR (fimp, FALSE); + + if (entry_num > 0) + { + gint i; + Itdb_Playlist *pl; + + pl = itdb_playlist_new (plname, FALSE); + /* Add new playlist */ + itdb_playlist_add (fimp->itdb, pl, -1); + + /* Add items */ + for (i=0; i<entry_num; ++i) + { + Itdb_Track *track; + guint32 num = get32lint (cts, + header_length + entry_length *i); + CHECK_ERROR (fimp, FALSE); + + track = g_list_nth_data (fimp->itdb->tracks, num); + if (track) + { + itdb_playlist_add_track (pl, track, -1); + } + else + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("OTG playlist file '%s': reference to non-existent track (%d)."), + cts->filename, num); + return FALSE; + } + } + } + return TRUE; +} + + + + +/* Add the On-The-Go Playlist(s) to the database */ +/* The OTG-Files are located in the directory given by + fimp->itdb->itdb_filename. + On error FALSE is returned and fimp->error is set accordingly. */ +static gboolean read_OTG_playlists (FImport *fimp) +{ + gchar *db[] = {"OTGPlaylistInfo", NULL}; + gchar *dirname, *otgname; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itdb->filename, FALSE); + + dirname = g_path_get_dirname (fimp->itdb->filename); + + otgname = itdb_resolve_path (dirname, (const gchar **)db); + + + /* only parse if "OTGPlaylistInfo" exists */ + if (otgname) + { + gchar *filename; + gint i=1; + do + { + db[0] = g_strdup_printf ("OTGPlaylistInfo_%d", i); + filename = itdb_resolve_path (dirname, (const gchar **)db); + g_free (db[0]); + if (filename) + { + FContents *cts = fcontents_read (filename, &fimp->error); + if (cts) + { + gchar *plname = g_strdup_printf (_("OTG Playlist %d"), i); + process_OTG_file (fimp, cts, plname); + g_free (plname); + fcontents_free (cts); + } + g_free (filename); + } + if (fimp->error) break; + ++i; + } while (filename); + g_free (otgname); + } + g_free (dirname); + return TRUE; +} + + + +static gboolean parse_fimp (FImport *fimp) +{ + glong seek=0, pl_mhsd=0; + guint32 i, zip, nr_tracks=0, nr_playlists=0; + gboolean swapped_mhsd = FALSE; + FContents *cts; + + g_return_val_if_fail (fimp, FALSE); + g_return_val_if_fail (fimp->itdb, FALSE); + g_return_val_if_fail (fimp->itunesdb, FALSE); + g_return_val_if_fail (fimp->itunesdb->filename, FALSE); + + cts = fimp->itunesdb; + + if (!cmp_n_bytes_seek (cts, "mhbd", 0, 4)) + { + if (cts->error) + { + g_propagate_error (&fimp->error, cts->error); + } + else + { /* set error */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("Not a iTunesDB: '%s' (missing mhdb header)."), + cts->filename); + } + return FALSE; + } + seek = get32lint (cts, 4); + CHECK_ERROR (fimp, FALSE); + /* all the headers I know are 0x68 long -- if this one is longer + we can could simply ignore the additional information */ + /* Since we only need data from the first 32 bytes, don't complain + * unless it's smaller than that */ + if (seek < 32) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB ('%s'): header length of mhsd hunk smaller than expected (%ld<32). Aborting."), + cts->filename, seek); + return FALSE; + } + + fimp->itdb->version = get32lint (cts, seek+16); + CHECK_ERROR (fimp, FALSE); + fimp->itdb->id = get64lint (cts, seek+24); + CHECK_ERROR (fimp, FALSE); + + for (;;) + { + if (cmp_n_bytes_seek (cts, "mhsd", seek, 4)) + { /* mhsd header -> determine start of playlists */ + guint32 sth; + guint32 len; + len = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + sth = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + if (sth == 1) + { /* OK, tracklist, save start of playlists */ + if (!swapped_mhsd) + pl_mhsd = seek + len; + } + else if (sth == 2) + { /* bad: these are playlists... switch */ + if (swapped_mhsd) + { /* already switched once -> forget it */ + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB '%s' corrupt: already found two playlist mhsds -- giving up."), + cts->filename); + return FALSE; + } + else + { + pl_mhsd = seek; + seek += len; + swapped_mhsd = TRUE; + } + } + else + { /* neither playlist nor track MHSD --> skip it */ + seek += len; + } + } + else + { /* if the cmp_n_bytes_seek() failed we must check if it is + because of an error */ + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhlt", seek, 4)) + { /* mhlt header -> number of tracks */ + nr_tracks = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + if (nr_tracks == 0) + { /* no tracks -- skip directly to next mhsd */ + break; + } + } + else + { /* if the cmp_n_bytes_seek() failed we must check if it is + because of an error */ + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhit", seek, 4)) + { /* mhit header -> start of tracks*/ + break; + } + zip = get32lint (cts, seek+4); + CHECK_ERROR (fimp, FALSE); + if (zip == 0) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, cts->filename); + return FALSE; + } + seek += zip; + } + /* now we should be at the first MHIT */ + + /* get every file entry */ + for (i=0; i<nr_tracks; ++i) + { + seek = get_mhit (fimp, seek); + if (fimp->error) return FALSE; + if (seek == -1) + { /* this should not be -- issue warning */ + g_warning (_("iTunesDB possibly corrupt: number of tracks (mhit hunks) inconsistent. Trying to continue.\n")); + break; + } + } + + /* next: playlists */ + seek = pl_mhsd; + for (;;) + { /* this is all a bit of magic to make sure we can handle + slightly "off-standard" iTunesDBs as well. Normally we + would expect hunks in the following order: <mhsd type 2>, + <mhlp> containing the number of playlists, <mhyp>: first + playlist header. Here we just scan everything until we find + the first <mhyp> ignoring everything we don't know. */ + zip = get32lint (cts, seek+4); + if (zip == 0) + { + g_set_error (&fimp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_CORRUPT, + _("iTunesDB corrupt: hunk length 0 for hunk at %ld in file '%s'."), + seek, cts->filename); + return FALSE; + } + CHECK_ERROR (fimp, FALSE); + if (cmp_n_bytes_seek (cts, "mhsd", seek, 4)) + { /* We just check if it's actually a playlist mhsd (type=2) + or not (type = 1, should not be...) */ + guint32 type; + guint32 len = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + type = get32lint (cts, seek+12); + CHECK_ERROR (fimp, FALSE); + if (type != 2) + { /* this is not a playlist MHSD -> skip it */ + seek += len; + continue; + } + else + { /* jump to next hunk */ + seek += zip; + continue; + } + } + else + { + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhlp", seek, 4)) + { /* mhlp header -> number of playlists */ + nr_playlists = get32lint (cts, seek+8); + CHECK_ERROR (fimp, FALSE); + seek += zip; + continue; + } + else + { + CHECK_ERROR (fimp, FALSE); + } + if (cmp_n_bytes_seek (cts, "mhyp", seek, 4)) + { /* mhyp header -> start of playlists */ + break; + } + else + { + CHECK_ERROR (fimp, FALSE); + } + seek += zip; + } + +#if ITUNESDB_DEBUG + fprintf(stderr, "iTunesDB part2 starts at: %x\n", (int)seek); +#endif + + /* Create track-id tree for quicker track lookup */ + fimp->idtree = itdb_track_id_tree_create (fimp->itdb); + for (i=0; i<nr_playlists; ++i) + { + seek = get_playlist (fimp, seek); + if (fimp->error) return FALSE; + if (seek == -1) + { /* this should not be -- issue warning */ + g_warning (_("iTunesDB possibly corrupt: number of playlists (mhyp hunks) inconsistent. Trying to continue.\n")); + break; + } + } + itdb_track_id_tree_destroy (fimp->idtree); + fimp->idtree = NULL; + + return TRUE; +} + + +/* Parse the Itdb_iTunesDB. + Returns a pointer to the Itdb_iTunesDB struct holding the tracks and the + playlists. + "mp" should point to the mount point of the iPod, + e.g. "/mnt/ipod" and be in local encoding */ +Itdb_iTunesDB *itdb_parse (const gchar *mp, GError **error) +{ + gchar *filename; + Itdb_iTunesDB *itdb = NULL; + const gchar *db[] = {"iPod_Control","iTunes","iTunesDB",NULL}; + + filename = itdb_resolve_path (mp, db); + if (filename) + { + itdb = itdb_parse_file (filename, error); + if (itdb) + { + itdb->mountpoint = g_strdup (mp); + } + g_free (filename); + } + else + { + gchar *str = g_build_filename (mp, db[0], db[1], db[2], db[3], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("File not found: '%s'."), + str); + g_free (str); + } + return itdb; +} + + +/* Same as itunesdb_parse(), but filename is specified directly. */ +Itdb_iTunesDB *itdb_parse_file (const gchar *filename, GError **error) +{ + FImport *fimp; + Itdb_iTunesDB *itdb; + gboolean success = FALSE; + + g_return_val_if_fail (filename, NULL); + + fimp = g_new0 (FImport, 1); + itdb = itdb_new (); + itdb->filename = g_strdup (filename); + fimp->itdb = itdb; + + fimp->itunesdb = fcontents_read (filename, error); + + if (fimp->itunesdb) + { + if (playcounts_init (fimp)) + { + if (parse_fimp (fimp)) + { + if (read_OTG_playlists (fimp)) + { + success = TRUE; + } + } + } + } + + if (!success) + { + itdb_free (itdb); + itdb = NULL; + if (fimp->error) + g_propagate_error (error, fimp->error); + } + itdb_free_fimp (fimp); + return itdb; +} + + +/* up to here we had the functions for reading the iTunesDB */ +/* ---------------------------------------------------------------------- */ +/* from here on we have the functions for writing the iTunesDB */ + +/* will expand @cts when necessary in order to accomodate @len bytes + starting at @seek */ +static void wcontents_maybe_expand (WContents *cts, gulong len, + gulong seek) +{ + g_return_if_fail (cts); + + while (cts->pos+len > cts->total) + { + cts->total += WCONTENTS_STEPSIZE; + cts->contents = g_realloc (cts->contents, cts->total); + } +} + + +/* Write @data, @n bytes long to position @seek. Will always be + * successful because glib terminates when out of memory */ +static void put_data_seek (WContents *cts, gchar *data, + gulong len, gulong seek) +{ + g_return_if_fail (cts); + g_return_if_fail (data); + + if (len != 0) + { + wcontents_maybe_expand (cts, len, seek); + + memcpy (&cts->contents[seek], data, len); + /* adjust end position if necessary */ + if (seek+len > cts->pos) + cts->pos = seek+len; + } +} + + + +/* Write @data, @n bytes long to end of @cts. Will always be + * successful because glib terminates when out of memory */ +static void put_data (WContents *cts, gchar *data, gulong len) +{ + g_return_if_fail (cts); + + put_data_seek (cts, data, len, cts->pos); +} + + +/* Write 1-byte integer @n to @cts */ +static void put8int (WContents *cts, guint8 n) +{ + put_data (cts, (gchar *)&n, 1); +} + + +/* Write 2-byte integer @n to @cts in little endian order. */ +static void put16lint (WContents *cts, guint16 n) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT16_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 2); +} + + +/* Write 4-byte integer @n to @cts in little endian order. */ +static void put32lint (WContents *cts, guint32 n) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 4); +} + + +/* Append @n times 2-byte-long zeros */ +static void put16_n0 (WContents *cts, gulong n) +{ + g_return_if_fail (cts); + + if (n>0) + { + wcontents_maybe_expand (cts, 2*n, cts->pos); + memset (&cts->contents[cts->pos], 0, 2*n); + cts->pos += 2*n; + } +} + +/* Write 3-byte integer @n to @cts in big endian order. */ +static void put24bint (WContents *cts, guint32 n) +{ + gchar buf[3] ; + buf[0] = (n >> 16) & 0xff ; + buf[1] = (n >> 8) & 0xff ; + buf[2] = (n >> 0) & 0xff ; + put_data (cts, buf, 3); +} + + +/* Write 4-byte integer @n to @cts at position @seek in little + endian order. */ +static void put32lint_seek (WContents *cts, guint32 n, gulong seek) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data_seek (cts, (gchar *)&n, 4, seek); +} + + +/* Write 8-byte integer @n to @cts in big little order. */ +static void put64lint (WContents *cts, guint64 n) +{ +# if (G_BYTE_ORDER == G_BIG_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 8); +} + + +/* Write 4-byte integer @n to @cts in big endian order. */ +static void put32bint (WContents *cts, guint32 n) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 4); +} + + +/* Write 8-byte integer @n to cts in big endian order. */ +static void put64bint (WContents *cts, guint64 n) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + put_data (cts, (gchar *)&n, 8); +} + + +#if 0 +/* Write 4-byte integer @n to @cts at position @seek in big endian + order. */ +static void put32bint_seek (WContents *cts, guint32 n, gulong seek) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT32_SWAP_LE_BE (n); +# endif + put_data_seek (cts, (gchar *)&n, 4, seek); +} + +/* Write 8-byte integer @n to @cts at position @seek in big endian + order. */ +static void put64bint_seek (WContents *cts, guint64 n, gulong seek) +{ +# if (G_BYTE_ORDER == G_LITTLE_ENDIAN) + n = GUINT64_SWAP_LE_BE (n); +# endif + put_data_seek (cts, (gchar *)&n, 8, seek); +} +#endif + + +/* Append @n times 4-byte-long zeros */ +static void put32_n0 (WContents *cts, gulong n) +{ + g_return_if_fail (cts); + + if (n>0) + { + wcontents_maybe_expand (cts, 4*n, cts->pos); + memset (&cts->contents[cts->pos], 0, 4*n); + cts->pos += 4*n; + } +} + + + +/* Write out the mhbd header. Size will be written later */ +static void mk_mhbd (FExport *fexp) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhbd", 4); + put32lint (cts, 104); /* header size */ + put32lint (cts, -1); /* size of whole mhdb -- fill in later */ + put32lint (cts, 1); /* ? */ + if (fexp->itdb->version < 0x09) fexp->itdb->version = 0x09; + /* Version number: 0x01: iTunes 2 + 0x02: iTunes 3 + 0x09: iTunes 4.2 + 0x0a: iTunes 4.5 + 0x0b: iTunes 4.7 + 0x0c: iTunes 4.71/4.8 (required for shuffle) + 0x0d: iTunes 4.9 */ + fexp->itdb->version = 0x0d; + put32lint (cts, fexp->itdb->version); + put32lint (cts, 2); /* 2 children (track list and playlist list) */ + put64lint (cts, fexp->itdb->id); + put32lint (cts, 2); /* ? */ + put32_n0 (cts, 17); /* dummy space */ +} + +/* Fill in the length of a standard header */ +static void fix_header (WContents *cts, gulong header_seek) +{ + put32lint_seek (cts, cts->pos-header_seek, header_seek+8); +} + + +/* Write out the mhsd header. Size will be written later */ +static void mk_mhsd (FExport *fexp, guint32 type) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhsd", 4); + put32lint (cts, 96); /* Headersize */ + put32lint (cts, -1); /* size of whole mhsd -- fill in later */ + put32lint (cts, type); /* type: 1 = track, 2 = playlist */ + put32_n0 (cts, 20); /* dummy space */ +} + + +/* Write out the mhlt header. */ +static void mk_mhlt (FExport *fexp, guint32 num) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itdb); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhlt", 4); + put32lint (cts, 92); /* Headersize */ + put32lint (cts, num); /* tracks in this itunesdb */ + put32_n0 (cts, 20); /* dummy space */ +} + + +/* Write out the mhit header. Size will be written later */ +static void mk_mhit (WContents *cts, Itdb_Track *track) +{ + g_return_if_fail (cts); + g_return_if_fail (track); + + put_data (cts, "mhit", 4); + put32lint (cts, 156); /* header size */ + put32lint (cts, -1); /* size of whole mhit -- fill in later */ + put32lint (cts, -1); /* nr of mhods in this mhit -- later */ + put32lint (cts, track->id); /* track index number + * */ + put32lint (cts, track->unk020); + put32lint (cts, track->unk024); + /* rating, compil., type */ + put32lint (cts, ((guint32)track->rating << 24) | + ((guint32)track->compilation << 16) | + ((guint32)track->type & 0x0000ffff)); + + put32lint (cts, track->time_added); /* timestamp */ + put32lint (cts, track->size); /* filesize */ + put32lint (cts, track->tracklen); /* length of track in ms */ + put32lint (cts, track->track_nr);/* track number */ + put32lint (cts, track->tracks); /* number of tracks */ + put32lint (cts, track->year); /* the year */ + put32lint (cts, track->bitrate); /* bitrate */ + put32lint (cts, track->samplerate << 16); + put32lint (cts, track->volume); /* volume adjust */ + put32lint (cts, track->starttime); + put32lint (cts, track->stoptime); + put32lint (cts, track->soundcheck); + put32lint (cts, track->playcount);/* playcount */ + put32lint (cts, track->unk084); + put32lint (cts, track->time_played); /* last time played */ + put32lint (cts, track->cd_nr); /* CD number */ + put32lint (cts, track->cds); /* number of CDs */ + put32lint (cts, track->unk100); + put32lint (cts, track->time_modified); /* timestamp */ + put32lint (cts, track->bookmark_time); + put64lint (cts, track->dbid); + if (track->checked) put8int (cts, 1); + else put8int (cts, 0); + put8int (cts, track->app_rating); + put16lint (cts, track->BPM); + put32lint (cts, track->unk124); + put32lint (cts, track->unk128); + put32lint (cts, track->unk132); + put32lint (cts, track->unk136); + put32lint (cts, track->unk140); + put32lint (cts, track->unk144); + put32lint (cts, track->unk148); + put32lint (cts, track->unk152); +} + + +/* Fill in the missing items of the mhit header: + total size and number of mhods */ +static void fix_mhit (WContents *cts, gulong mhit_seek, guint32 mhod_num) +{ + g_return_if_fail (cts); + + /* size of whole mhit */ + put32lint_seek (cts, cts->pos-mhit_seek, mhit_seek+8); + /* nr of mhods */ + put32lint_seek (cts, mhod_num, mhit_seek+12); +} + + +/* Write out one mhod header. + type: see enum of MHMOD_IDs; + data: utf8 string for text items + position indicator for MHOD_ID_PLAYLIST + SPLPref for MHOD_ID_SPLPREF + SPLRules for MHOD_ID_SPLRULES */ +static void mk_mhod (WContents *cts, enum MHOD_ID type, void *data) +{ + g_return_if_fail (cts); + + switch (type) + { + case MHOD_ID_TITLE: + case MHOD_ID_PATH: + case MHOD_ID_ALBUM: + case MHOD_ID_ARTIST: + case MHOD_ID_GENRE: + case MHOD_ID_FDESC: + case MHOD_ID_COMMENT: + case MHOD_ID_COMPOSER: + case MHOD_ID_GROUPING: + g_return_if_fail (data); + { + /* convert to utf16 */ + gunichar2 *entry_utf16 = g_utf8_to_utf16 ((gchar *)data, -1, + NULL, NULL, NULL); + guint32 len = utf16_strlen (entry_utf16); + fixup_little_utf16 (entry_utf16); + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 2*len+40); /* size of header + body */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, 1); /* always 1 for these MHOD_IDs*/ + put32lint (cts, 2*len); /* size of string */ + put32_n0 (cts, 2); /* unknown */ + put_data (cts, (gchar *)entry_utf16, 2*len); /* the string */ + g_free (entry_utf16); + } + break; + case MHOD_ID_PLAYLIST: + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 44); /* size of header + body */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put32lint (cts, (guint32)data);/* position of track in playlist */ + put32_n0 (cts, 4); /* unknown */ + break; + case MHOD_ID_SPLPREF: + g_return_if_fail (data); + { + SPLPref *splp = data; + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, 96); /* size of header + body */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + put8int (cts, splp->liveupdate); + put8int (cts, splp->checkrules? 1:0); + put8int (cts, splp->checklimits); + put8int (cts, splp->limittype); + put8int (cts, splp->limitsort & 0xff); + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put32lint (cts, splp->limitvalue); + put8int (cts, splp->matchcheckedonly); + /* for the following see note at definitions of limitsort + types in itunesdb.h */ + put8int (cts, (splp->limitsort & 0x80000000) ? 1:0); + put8int (cts, 0); /* unknown */ + put8int (cts, 0); /* unknown */ + put32_n0 (cts, 14); /* unknown */ + } + break; + case MHOD_ID_SPLRULES: + g_return_if_fail (data); + { + SPLRules *splrs = data; + gulong header_seek = cts->pos; /* needed to fix length */ + GList *gl; + gint numrules = g_list_length (splrs->rules); + + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 24); /* size of header */ + put32lint (cts, -1); /* total length, fix later */ + put32lint (cts, type); /* type of the entry */ + put32_n0 (cts, 2); /* unknown */ + /* end of header, start of data */ + /* For some reason this is the only part of the iTunesDB + that uses big endian */ + put_data (cts, "SLst", 4); /* header */ + put32bint (cts, splrs->unk004); /* unknown */ + put32bint (cts, numrules); + put32bint (cts, splrs->match_operator); + put32_n0 (cts, 30); /* unknown */ + /* end of header, now follow the rules */ + for (gl=splrs->rules; gl; gl=gl->next) + { + SPLRule *splr = gl->data; + gint ft; + g_return_if_fail (splr); + ft = itdb_splr_get_field_type (splr); +/* printf ("%p: field: %d ft: %d\n", splr, splr->field, ft);*/ + itdb_splr_validate (splr); + put32bint (cts, splr->field); + put32bint (cts, splr->action); + put32_n0 (cts, 11); /* unknown */ + if (ft == splft_string) + { /* write string-type rule */ + gunichar2 *entry_utf16 = + g_utf8_to_utf16 (splr->string, -1,NULL,NULL,NULL); + gint len = utf16_strlen (entry_utf16); + fixup_big_utf16 (entry_utf16); + put32bint (cts, 2*len); /* length of string */ + put_data (cts, (gchar *)entry_utf16, 2*len); + g_free (entry_utf16); + } + else + { /* write non-string-type rule */ + put32bint (cts, 0x44); /* length of data */ + /* data */ + put64bint (cts, splr->fromvalue); + put64bint (cts, splr->fromdate); + put64bint (cts, splr->fromunits); + put64bint (cts, splr->tovalue); + put64bint (cts, splr->todate); + put64bint (cts, splr->tounits); + put32bint (cts, splr->unk052); + put32bint (cts, splr->unk056); + put32bint (cts, splr->unk060); + put32bint (cts, splr->unk064); + put32bint (cts, splr->unk068); + } + } + /* insert length of mhod junk */ + fix_header (cts, header_seek); + } + break; + case MHOD_ID_MHYP: + g_warning (_("Cannot write mhod of type %d\n"), type); + break; + } +} + + +/* Write out the mhlp header. Size will be written later */ +static void mk_mhlp (FExport *fexp) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhlp", 4); /* header */ + put32lint (cts, 92); /* size of header */ + /* playlists on iPod (including main!) */ + put32lint (cts, g_list_length (fexp->itdb->playlists)); + put32_n0 (cts, 20); /* dummy space */ +} + + +/* Write out the long MHOD_ID_PLAYLIST mhod header. + This seems to be an itunespref thing.. dunno know this + but if we set everything to 0, itunes doesn't show any data + even if you drag an mp3 to your ipod: nothing is shown, but itunes + will copy the file! + .. so we create a hardcoded-pref.. this will change in future + Seems to be a Preferences mhod, every PL has such a thing + FIXME !!! */ +static void mk_long_mhod_id_playlist (FExport *fexp, Itdb_Playlist *pl) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itunesdb); + g_return_if_fail (pl); + + cts = fexp->itunesdb; + + put_data (cts, "mhod", 4); /* header */ + put32lint (cts, 0x18); /* size of header ? */ + put32lint (cts, 0x0288); /* size of header + body */ + put32lint (cts, MHOD_ID_PLAYLIST); /* type of the entry */ + put32_n0 (cts, 6); + put32lint (cts, 0x010084); /* ? */ + put32lint (cts, 0x05); /* ? */ + put32lint (cts, 0x09); /* ? */ + put32lint (cts, 0x03); /* ? */ + put32lint (cts, 0x120001); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0xc80002); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x3c000d); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x7d0004); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x7d0003); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x640008); /* ? */ + put32_n0 (cts, 3); + put32lint (cts, 0x640017); /* ? */ + put32lint (cts, 0x01); /* bool? (visible? / colums?) */ + put32_n0 (cts, 2); + put32lint (cts, 0x500014); /* ? */ + put32lint (cts, 0x01); /* bool? (visible?) */ + put32_n0 (cts, 2); + put32lint (cts, 0x7d0015); /* ? */ + put32lint (cts, 0x01); /* bool? (visible?) */ + put32_n0 (cts, 114); +} + + + +/* Header for new PL item */ +/* @pos: position in playlist */ +static void mk_mhip (FExport *fexp, guint32 pos, guint32 id) +{ + WContents *cts; + + g_return_if_fail (fexp); + g_return_if_fail (fexp->itunesdb); + + cts = fexp->itunesdb; + + put_data (cts, "mhip", 4); + put32lint (cts, 76); /* 4 */ + put32lint (cts, -1); /* fill in later */ /* 8 */ + put32lint (cts, 1); /* number of children */ /* 12 */ + put32lint (cts, 0); /* unknown */ /* 16 */ + put32lint (cts, 0); /* unknown */ /* 20 */ + put32lint (cts, id); /* id */ /* 24 */ + put32_n0 (cts, 12); /* 28 */ +} + + +/* Write first mhsd hunk. Return FALSE in case of error and set + * fexp->error */ +static gboolean write_mhsd_one(FExport *fexp) +{ + GList *gl; + gulong mhsd_seek; + WContents *cts; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->itunesdb, FALSE); + + cts = fexp->itunesdb; + + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 1); /* write header: type 1: tracks */ + /* write header with nr. of tracks */ + mk_mhlt (fexp, g_list_length (fexp->itdb->tracks)); + for (gl=fexp->itdb->tracks; gl; gl=gl->next) /* Write each track */ + { + Itdb_Track *track = gl->data; + guint32 mhod_num = 0; + gulong mhit_seek = cts->pos; + if (!track) + { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Database in memory corrupt (track pointer == NULL). Aborting export.")); + return FALSE; + } + mk_mhit (cts, track); + if (track->title && *track->title) + { + mk_mhod (cts, MHOD_ID_TITLE, track->title); + ++mhod_num; + } + if (track->ipod_path && *track->ipod_path) + { + mk_mhod (cts, MHOD_ID_PATH, track->ipod_path); + ++mhod_num; + } + if (track->album && *track->album) + { + mk_mhod (cts, MHOD_ID_ALBUM, track->album); + ++mhod_num; + } + if (track->artist && *track->artist) + { + mk_mhod (cts, MHOD_ID_ARTIST, track->artist); + ++mhod_num; + } + if (track->genre && *track->genre) + { + mk_mhod (cts, MHOD_ID_GENRE, track->genre); + ++mhod_num; + } + if (track->fdesc && *track->fdesc) + { + mk_mhod (cts, MHOD_ID_FDESC, track->fdesc); + ++mhod_num; + } + if (track->comment && *track->comment) + { + mk_mhod (cts, MHOD_ID_COMMENT, track->comment); + ++mhod_num; + } + if (track->composer && *track->composer) + { + mk_mhod (cts, MHOD_ID_COMPOSER, track->composer); + ++mhod_num; + } + if (track->grouping && *track->grouping) + { + mk_mhod (cts, MHOD_ID_GROUPING, track->grouping); + ++mhod_num; + } + /* Fill in the missing items of the mhit header */ + fix_mhit (cts, mhit_seek, mhod_num); + } + fix_header (cts, mhsd_seek); + return TRUE; +} + +/* corresponds to mk_mhyp */ +/* Return FALSE in case of error and set fexp->error */ +static gboolean write_playlist(FExport *fexp, Itdb_Playlist *pl) +{ + GList *gl; + gulong mhyp_seek, mhip_seek; + guint32 i; + WContents *cts; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->itunesdb, FALSE); + g_return_val_if_fail (pl, FALSE); + + cts = fexp->itunesdb; + mhyp_seek = cts->pos; + +#if ITUNESDB_DEBUG + fprintf(stderr, "Playlist: %s (%d tracks)\n", pl->name, g_list_length (pl->members)); +#endif + + put_data (cts, "mhyp", 4); /* header */ + put32lint (cts, 108); /* length */ + put32lint (cts, -1); /* size -> later */ + if (pl->is_spl) + put32lint (cts, 4); /* nr of mhods */ + else + put32lint (cts, 2); /* nr of mhods */ + /* number of tracks in plist */ + put32lint (cts, g_list_length (pl->members)); + put32lint (cts, pl->type); /* 1 = main, 0 = visible */ + put32lint (cts, 0); /* some timestamp */ + put64lint (cts, pl->id); /* 64 bit ID */ + put32lint (cts, pl->unk036); + put32lint (cts, pl->unk040); + put32lint (cts, pl->unk044); + put32_n0 (cts, 15); /* ? */ + + mk_mhod (cts, MHOD_ID_TITLE, pl->name); + mk_long_mhod_id_playlist (fexp, pl); + + if (pl->is_spl) + { /* write the smart rules */ + mk_mhod (cts, MHOD_ID_SPLPREF, &pl->splpref); + mk_mhod (cts, MHOD_ID_SPLRULES, &pl->splrules); + } + + /* write hard-coded tracks */ + i=0; + for (gl=pl->members; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + if (!track) + { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Database in memory corrupt (track pointer == NULL). Aborting export.")); + return FALSE; + } + mhip_seek = cts->pos; + mk_mhip (fexp, i, track->id); + mk_mhod (cts, MHOD_ID_PLAYLIST, (void *)i); + /* note: with iTunes 4.9 the mhod is counted as a child to + mhip, so we fill have put the total length of the mhip and + mhod into the mhip header */ + fix_header (cts, mhip_seek); + ++i; + } + fix_header (cts, mhyp_seek); + return TRUE; +} + + + +/* Expects the master playlist to be the first in the list */ +/* Return FALSE in case of error and set fexp->error */ +static gboolean write_mhsd_two(FExport *fexp) +{ + GList *gl; + glong mhsd_seek; + WContents *cts; + + g_return_val_if_fail (fexp, FALSE); + g_return_val_if_fail (fexp->itdb, FALSE); + g_return_val_if_fail (fexp->itunesdb, FALSE); + + cts = fexp->itunesdb; + mhsd_seek = cts->pos; /* get position of mhsd header */ + mk_mhsd (fexp, 2); /* write header: type 2: playlists */ + /* write header with nr. of playlists */ + mk_mhlp (fexp); + for(gl=fexp->itdb->playlists; gl; gl=gl->next) + { + Itdb_Playlist *pl = gl->data; + if (!pl) + { + g_set_error (&fexp->error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_ITDB_CORRUPT, + _("Database in memory corrupt (playlist pointer == NULL). Aborting export.")); + return FALSE; + } + write_playlist (fexp, pl); + if (fexp->error) return FALSE; + } + fix_header (cts, mhsd_seek); + return TRUE; +} + + +/* create a WContents structure */ +static WContents *wcontents_new (const gchar *filename) +{ + WContents *cts; + + g_return_val_if_fail (filename, NULL); + + cts = g_new0 (WContents, 1); + cts->filename = g_strdup (filename); + + return cts; +} + + +/* write the contents of WContents. Return FALSE on error and set + * cts->error accordingly. */ +static gboolean wcontents_write (WContents *cts) +{ + int fd; + + g_return_val_if_fail (cts, FALSE); + g_return_val_if_fail (cts->filename, FALSE); + + fd = creat (cts->filename, S_IRWXU|S_IRWXG|S_IRWXO); + + if (fd == -1) + { + cts->error = g_error_new (G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Opening of '%s' for writing failed."), + cts->filename); + return FALSE; + } + if (cts->contents && cts->pos) + { + ssize_t written = write (fd, cts->contents, cts->pos); + if (written == -1) + { + cts->error = g_error_new (G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Writing to '%s' failed."), + cts->filename); + close (fd); + return FALSE; + } + } + fd = close (fd); + if (fd == -1) + { + cts->error = g_error_new (G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Writing to '%s' failed (%s)."), + cts->filename, g_strerror (errno)); + return FALSE; + } + return TRUE; +} + + +/* Free memory associated with WContents @cts */ +static void wcontents_free (WContents *cts) +{ + if (cts) + { + g_free (cts->filename); + g_free (cts->contents); + /* must not g_error_free (cts->error) because the error was + propagated -> might free the error twice */ + g_free (cts); + } +} + + +/* reassign the iPod IDs and make sure the itdb->tracks are in the + same order as the mpl */ +static void reassign_ids (Itdb_iTunesDB *itdb) +{ + guint32 id = 52; + GList *gl; + Itdb_Playlist *mpl; + + g_return_if_fail (itdb); + + /* copy mpl->members to itdb->tracks to make sure they are in the + same order (otherwise On-The-Go Playlists will not show the + correct content) */ + mpl = itdb_playlist_mpl (itdb); + g_return_if_fail (mpl); + g_return_if_fail (g_list_length (mpl->members) == g_list_length (itdb->tracks)); + g_list_free (itdb->tracks); + itdb->tracks = g_list_copy (mpl->members); + + /* assign unique IDs */ + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + g_return_if_fail (track); + track->id = id++; + } +} + + + +/* Do the actual writing to the iTunesDB */ +/* If @filename==NULL, itdb->filename is tried */ +gboolean itdb_write_file (Itdb_iTunesDB *itdb, const gchar *filename, + GError **error) +{ + FExport *fexp; + gulong mhbd_seek = 0; + WContents *cts; + gboolean result = TRUE;; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (filename || itdb->filename, FALSE); + + if (!filename) filename = itdb->filename; + + reassign_ids (itdb); + + fexp = g_new0 (FExport, 1); + fexp->itdb = itdb; + fexp->itunesdb = wcontents_new (filename); + cts = fexp->itunesdb; + + mk_mhbd (fexp); + if (write_mhsd_one(fexp)) + { /* write playlists mhsd */ + if (write_mhsd_two(fexp)) + { + fix_header (cts, mhbd_seek); + } + } + if (!fexp->error) + { + if (!wcontents_write (cts)) + g_propagate_error (&fexp->error, cts->error); + } + if (fexp->error) + { + g_propagate_error (error, fexp->error); + result = FALSE; + } + wcontents_free (cts); + g_free (fexp); + if (result == TRUE) + { + gchar *fn = g_strdup (filename); + g_free (itdb->filename); + itdb->filename = fn; + } + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + +/* Write out an iTunesDB. + + First reassigns unique IDs to all tracks. + + An existing "Play Counts" file is renamed to "Play Counts.bak" if + the export was successful. + + An existing "OTGPlaylistInfo" file is removed if the export was + successful. + + Returns TRUE on success, FALSE on error, in which case @error is + set accordingly. + + @mp must point to the mount point of the iPod, e.g. "/mnt/ipod" and + be in local encoding. If mp==NULL, itdb->mountpoint is tried. */ +gboolean itdb_write (Itdb_iTunesDB *itdb, const gchar *mp, GError **error) +{ + gchar *itunes_filename, *itunes_path; + const gchar *db[] = {"iPod_Control","iTunes",NULL}; + gboolean result = FALSE; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (mp || itdb->mountpoint, FALSE); + + if (!mp) mp = itdb->mountpoint; + + itunes_path = itdb_resolve_path (mp, db); + + if(!itunes_path) + { + gchar *str = g_build_filename (mp, db[0], db[1], db[2], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + itunes_filename = g_build_filename (itunes_path, "iTunesDB", NULL); + + result = itdb_write_file (itdb, itunes_filename, error); + + g_free(itunes_filename); + g_free(itunes_path); + + if (result == TRUE) + result = itdb_rename_files (mp, error); + + if (result == TRUE) + { + gchar *mnp = g_strdup (mp); + g_free (itdb->mountpoint); + itdb->mountpoint = mnp; + } + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + + +/* from here on we have the functions for writing the iTunesDB */ +/* -------------------------------------------------------------------- */ +/* up to here we had the functions for writing the iTunesSD */ + +/* +| Copyright (C) 2005 Jorg Schuler <jcsjcs at users.sourceforge.net> +| Part of the gtkpod project. +| +| Based on itunessd.c written by Steve Wahl for gtkpod-0.88: +| +| Copyright 2005 Steve Wahl <steve at pro-ns dot net> +| +| This file contains routines to create the iTunesSD file, as +| used by the ipod shuffle. +| +| Like itunesdb.c, it is derived from the perl script "mktunes.pl" +| (part of the gnupod-tools collection) written by Adrian +| Ulrich <pab at blinkenlights.ch>. +| +| Small(?) portions derived from itunesdb.c, so Jorg Schuler probably +| has some copyright ownership in this file as well. +| +| The code contained in this file is free software; you can redistribute +| it and/or modify it under the terms of the GNU Lesser General Public +| License as published by the Free Software Foundation; either version +| 2.1 of the License, or (at your option) any later version. +| +| This file 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 +| Lesser General Public License for more details. +| +| You should have received a copy of the GNU Lesser General Public +| License along with this code; if not, write to the Free Software +| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +*/ + +/* notes: + + All software currently seems to write iTunesDB as well as iTunesSD + on the iPod shuffle. I assume that reading from the iTunesSD file + is not necessary. The iTunesStats file is different, but I leave + that for another day. + + The iTunesSD file format is as follows (taken from WikiPodLinux, feb + '05): + + Offset Field Bytes Value + (hex) (dec) + + iTunesSD header (occurs once, at beginning of file): + + 00 num_songs 3 number of song entries in the file + + 03 unknown 3 always(?) 0x010600 + 06 header size 3 size of the header (0x12, 18 bytes) + 09 unknown 3 possibly zero padding + + iTunesSD song entry format (occurs once for each song) + + 000 size of entry 3 always(?) 0x00022e (558 bytes) + 003 unk1 3 unknown, always(?) 0x5aa501 + 006 starttime 3 Start Time, in 256 ms increments + e.g. 60s = 0xea (234 dec) + 009 unk2 3 unknown (always 0?) + 00C unk3 3 unknown, some relationship to starttime + 00F stoptime 3 Stop Time, also in 256 ms increments. + Zero means play to end of file. + 012 unk4 3 Unknown. + 015 unk5 3 Unknown, but associated with stoptime? + 018 volume 3 Volume - ranges from 0x00 (-100%) to 0x64 + (0%) to 0xc8 (100%) + 01B file_type 3 0x01 = MP3, 0x02 = AAC, 0x04=WAV + 01E unk6 3 unknown (always 0x200?) + 021 filename 522 filename of the song, padded at the end + with 0's. Note: forward slashes are used + here, not colons like in the iTunesDB -- + for example, + "/iPod_Control/Music/F00/Song.mp3" + 22B shuffleflag 1 If this value is 0x00, the song will be + skipped while the player is in shuffle + mode. Any other value will allow it to be + played in both normal and shuffle modes. + iTunes 4.7.1 sets this to 0 for audio books. + 22C bookmarkflag 1 If this flag is 0x00, the song will not be + bookmarkable (i.e. its playback position + won't be saved when switching to a different + song). Any other value wil make it + Bookmarkable. Unlike hard drive based iPods, + all songs can be marked as bookmarkable, + not just .m4b and .aa + 22D unknownflag 1 unknown, always? 0x00. + +All integers in the iTunesSD file are in BIG endian form... + +*/ + + +/* Write out an iTunesSD for the Shuffle. + + First reassigns unique IDs to all tracks. + + An existing "Play Counts" file is renamed to "Play Counts.bak" if + the export was successful. + + An existing "OTGPlaylistInfo" file is removed if the export was + successful. + + Returns TRUE on success, FALSE on error, in which case @error is + set accordingly. + + @mp must point to the mount point of the iPod, e.g. "/mnt/ipod" and + be in local encoding. If mp==NULL, itdb->mountpoint is tried. */ + + +gboolean itdb_shuffle_write (Itdb_iTunesDB *itdb, + const gchar *mp, GError **error) +{ + gchar *itunes_filename, *itunes_path; + const gchar *db[] = {"iPod_Control","iTunes",NULL}; + gboolean result = FALSE; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (mp || itdb->mountpoint, FALSE); + + if (!mp) mp = itdb->mountpoint; + + itunes_path = itdb_resolve_path (mp, db); + + if(!itunes_path) + { + gchar *str = g_build_filename (mp, db[0], db[1], db[2], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + itunes_filename = g_build_filename (itunes_path, "iTunesSD", NULL); + + result = itdb_shuffle_write_file (itdb, itunes_filename, error); + + g_free(itunes_filename); + g_free(itunes_path); + + if (result == TRUE) + result = itdb_rename_files (mp, error); + + if (result == TRUE) + { + gchar *mnp = g_strdup (mp); + g_free (itdb->mountpoint); + itdb->mountpoint = mnp; + } + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + + +/* Do the actual writing to the iTunesSD */ +/* If @filename cannot be NULL */ +gboolean itdb_shuffle_write_file (Itdb_iTunesDB *itdb, + const gchar *filename, GError **error) +{ + gboolean haystack (gchar *fdesc, gchar **desclist) + { + gchar **dlp; + if (!fdesc || !desclist) return FALSE; + for (dlp=desclist; *dlp; ++dlp) + { + if (strstr (fdesc, *dlp)) return TRUE; + } + return FALSE; + } + + FExport *fexp; + GList *gl; + WContents *cts; + gboolean result = TRUE;; + + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (filename, FALSE); + + reassign_ids (itdb); + + fexp = g_new0 (FExport, 1); + fexp->itdb = itdb; + fexp->itunesdb = wcontents_new (filename); + cts = fexp->itunesdb; + + put24bint (cts, itdb_tracks_number (itdb)); + put24bint (cts, 0x010600); + put24bint (cts, 0x12); /* size of header */ + put24bint (cts, 0x0); /* padding? */ + put24bint (cts, 0x0); + put24bint (cts, 0x0); + + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *tr = gl->data; + gchar *path; + gunichar2 *path_utf16; + guint32 pathlen; + gchar *mp3_desc[] = {"MPEG", "MP3", "mpeg", "mp3", NULL}; + gchar *mp4_desc[] = {"AAC", "MP4", "aac", "mp4", NULL}; + gchar *wav_desc[] = {"WAV", "wav", NULL}; + + g_return_val_if_fail (tr, FALSE); + + put24bint (cts, 0x00022e); + put24bint (cts, 0x5aa501); + /* starttime is in 256 ms incr. for shuffle */ + put24bint (cts, tr->starttime / 256); + put24bint (cts, 0); + put24bint (cts, 0); + put24bint (cts, tr->stoptime / 256); + put24bint (cts, 0); + put24bint (cts, 0); + /* track->volume ranges from -255 to +255 */ + /* we want 0 - 200 */ + put24bint (cts, ((tr->volume + 255) * 201) / 511); + + /* The next one should be 0x01 for MP3, + ** 0x02 for AAC, and 0x04 for WAV, but I can't find + ** a suitable indicator within the track structure? */ + /* JCS: let's do heuristic on tr->fdesc which would contain + "MPEG audio file", "AAC audio file", "Protected AAC audio + file", "AAC audio book file", "WAV audio file" (or similar + if not written by gtkpod */ + + if (haystack (tr->fdesc, mp3_desc)) + put24bint (cts, 0x01); + else if (haystack (tr->fdesc, mp4_desc)) + put24bint (cts, 0x02); + else if (haystack (tr->fdesc, wav_desc)) + put24bint (cts, 0x04); + else + put24bint (cts, 0x01); /* default to mp3 */ + + put24bint (cts, 0x200); + + /* shuffle uses forward slash separator, not colon */ + path = g_strdup (tr->ipod_path); + itdb_filename_ipod2fs (path); + path_utf16 = g_utf8_to_utf16 (path, -1, NULL, NULL, NULL); + pathlen = utf16_strlen (path_utf16); + if (pathlen > 261) pathlen = 261; + fixup_little_utf16 (path_utf16); + put_data (cts, (gchar *)path_utf16, 2*pathlen); + /* pad to 522 bytes */ + put16_n0 (cts, 261-pathlen); + g_free(path); + g_free(path_utf16); + + /* XXX FIXME: should depend on something, not hardcoded */ + put8int (cts, 0x1); /* song used in shuffle mode */ + put8int (cts, 0); /* song will not be bookmarkable */ + put8int (cts, 0); + } + if (!fexp->error) + { + if (!wcontents_write (cts)) + g_propagate_error (&fexp->error, cts->error); + } + if (fexp->error) + { + g_propagate_error (error, fexp->error); + result = FALSE; + } + wcontents_free (cts); + g_free (fexp); + + /* make sure all buffers are flushed as some people tend to + disconnect as soon as gtkpod returns */ + sync (); + + return result; +} + + + + + + + + + + + +/*------------------------------------------------------------------*\ + * * + * Other file/filename stuff * + * * +\*------------------------------------------------------------------*/ + + +/* (Renames/removes some files on the iPod (Playcounts, OTG + semaphore). May have to be called if you write the iTunesDB not + directly to the iPod but to some other location and then manually + copy the file from there to the iPod. */ +/* Returns FALSE on error and sets @error accordingly */ +gboolean itdb_rename_files (const gchar *mp, GError **error) +{ + const gchar *db_itd[] = {"iPod_Control", "iTunes", NULL}; + const gchar *db_plc_o[] = {"Play Counts", NULL}; + const gchar *db_otg[] = {"OTGPlaylistInfo", NULL}; + gchar *itunesdir; + gchar *plcname_o; + gchar *plcname_n; + gchar *otgname; + gboolean result = TRUE; + + itunesdir = itdb_resolve_path (mp, db_itd); + if(!itunesdir) + { + gchar *str = g_build_filename (mp, db_itd[0], + db_itd[1], db_itd[2], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + + plcname_o = itdb_resolve_path (itunesdir, db_plc_o); + plcname_n = g_build_filename (itunesdir, + "Play Counts.bak", NULL); + otgname = itdb_resolve_path (itunesdir, db_otg); + + /* rename "Play Counts" to "Play Counts.bak" */ + if (plcname_o) + { + if (rename (plcname_o, plcname_n) == -1) + { /* an error occured */ + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error renaming '%s' to '%s' (%s)."), + plcname_o, plcname_n, g_strerror (errno)); + result = FALSE; + } + } + + /* remove "OTGPlaylistInfo" (the iPod will remove the remaining + * files */ + if (otgname) + { + if (unlink (otgname) == -1) + { + if (error && !*error) + { /* don't overwrite previous error */ + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error removing '%s' (%s)."), + otgname, g_strerror (errno)); + } + result = FALSE; + } + } + + g_free (plcname_o); + g_free (plcname_n); + g_free (otgname); + g_free (itunesdir); + + return result; +} + + +/* Convert string from casual PC file name to iPod iTunesDB format + * using ':' instead of slashes + */ +void itdb_filename_fs2ipod (gchar *ipod_file) +{ + g_strdelimit (ipod_file, G_DIR_SEPARATOR_S, ':'); +} + +/* Convert string from iPod iTunesDB format to casual PC file name + * using slashes instead of ':' + */ +void itdb_filename_ipod2fs (gchar *ipod_file) +{ + g_strdelimit (ipod_file, ":", G_DIR_SEPARATOR); +} + + + +/* Copy one track to the iPod. The PC filename is @filename + and is taken literally. + @path is the mountpoint of the iPod (in local encoding). + + If @track->transferred is set to TRUE, nothing is done. Upon + successful transfer @track->transferred is set to TRUE. + + For storage, the directories "f00 ... f19" will be + cycled through. + + The filename is constructed as "gtkpod"<random number> and copied + to @track->ipod_path. If this file already exists, <random number> + is adjusted until an unused filename is found. + + If @track->ipod_path is already set, this one will be used + instead. If a file with this name already exists, it will be + overwritten. */ +gboolean itdb_cp_track_to_ipod (const gchar *mp, Itdb_Track *track, + gchar *filename, GError **error) +{ + static gint dir_num = -1; + gchar *track_db_path, *ipod_fullfile; + gboolean success; + gint mplen = 0; + + g_return_val_if_fail (mp, FALSE); + g_return_val_if_fail (track, FALSE); + g_return_val_if_fail (filename, FALSE); + + if(track->transferred) return TRUE; /* nothing to do */ + + /* If track->ipod_path exists, we use that one instead. */ + ipod_fullfile = itdb_filename_on_ipod (mp, track); + + if (!ipod_fullfile) + { + gchar *dest_components[] = {"iPod_Control", "Music", + NULL, NULL, NULL}; + gchar *parent_dir_filename; + gchar *original_suffix; + gchar dir_num_str[5]; + gint32 oops = 0; + gint32 rand = g_random_int_range (0, 899999); /* 0 to 900000 */ + + if (dir_num == -1) dir_num = g_random_int_range (0, 20); + else dir_num = (dir_num + 1) % 20; + + g_snprintf (dir_num_str, 5, "F%02d", dir_num); + dest_components[2] = dir_num_str; + + parent_dir_filename = + itdb_resolve_path (mp, (const gchar **)dest_components); + + if(parent_dir_filename == NULL) + { + /* Can't find the F%02d directory */ + gchar *str = g_build_filename (mp, dest_components[0], + dest_components[1], + dest_components[2], + dest_components[3], NULL); + g_set_error (error, + ITDB_FILE_ERROR, + ITDB_FILE_ERROR_NOTFOUND, + _("Path not found: '%s'."), + str); + g_free (str); + return FALSE; + } + + /* we need the original suffix of pcfile to construct a correct ipod + filename */ + original_suffix = strrchr (filename, '.'); + /* If there is no ".mp3", ".m4a" etc, set original_suffix to empty + string. Note: the iPod will most certainly ignore this file... */ + if (!original_suffix) original_suffix = ""; + + do + { /* we need to loop until we find an unused filename */ + dest_components[3] = + g_strdup_printf("gtkpod%06d%s", + rand + oops, original_suffix); + ipod_fullfile = itdb_resolve_path ( + parent_dir_filename, + (const gchar **)&dest_components[3]); + if(ipod_fullfile) + { /* already exists -- try next */ + g_free(ipod_fullfile); + ipod_fullfile = NULL; + } + else + { /* found unused file -- build filename */ + ipod_fullfile = g_build_filename (parent_dir_filename, + dest_components[3], NULL); + } + g_free (dest_components[3]); + ++oops; + } while (!ipod_fullfile); + g_free(parent_dir_filename); + } + /* now extract filepath for track->ipod_path from ipod_fullfile */ + /* ipod_path must begin with a '/' */ + mplen = strlen (mp); /* length of mountpoint in bytes */ + if (ipod_fullfile[mplen] == G_DIR_SEPARATOR) + { + track_db_path = g_strdup (&ipod_fullfile[mplen]); + } + else + { + track_db_path = g_strdup_printf ("%c%s", G_DIR_SEPARATOR, + &ipod_fullfile[mplen]); + } + /* convert to iPod type */ + itdb_filename_fs2ipod (track_db_path); + +/* printf ("ff: %s\ndb: %s\n", ipod_fullfile, track_db_path); */ + + success = itdb_cp (filename, ipod_fullfile, error); + if (success) + { + track->transferred = TRUE; + g_free (track->ipod_path); + track->ipod_path = g_strdup (track_db_path); + } + + g_free (track_db_path); + g_free (ipod_fullfile); + return success; +} + + +/* Return the full iPod filename as stored in @track. Return value + must be g_free()d after use. + @mp: mount point of the iPod file system (in local encoding) + @track: track + Return value: full filename to @track on the iPod or NULL if no + filename is set in @track. + + NOTE: NULL is returned when the file does not exist. + + NOTE: this code works around a problem on some systems (see + itdb_resolve_path() ) and might return a filename with different + case than the original filename. Don't copy it back to @track + unless you must */ +gchar *itdb_filename_on_ipod (const gchar *mp, Itdb_Track *track) +{ + gchar *result = NULL; + + g_return_val_if_fail (track, NULL); + + if(track->ipod_path && *track->ipod_path) + { + gchar *buf = g_strdup (track->ipod_path); + itdb_filename_ipod2fs (buf); + result = g_build_filename (mp, buf, NULL); + g_free (buf); + if (!g_file_test (result, G_FILE_TEST_EXISTS)) + { + gchar **components = g_strsplit (track->ipod_path,":",10); + g_free (result); + result = itdb_resolve_path (mp, (const gchar **)components); + g_strfreev (components); + } + } + return result; +} + + +/* Copy file "from_file" to "to_file". + Returns TRUE on success, FALSE otherwise */ +gboolean itdb_cp (const gchar *from_file, const gchar *to_file, + GError **error) +{ + gchar *data; + glong bread, bwrite; + FILE *file_in = NULL; + FILE *file_out = NULL; + +#if ITUNESDB_DEBUG + fprintf(stderr, "Entered itunesdb_cp: '%s', '%s'\n", from_file, to_file); +#endif + + g_return_val_if_fail (from_file, FALSE); + g_return_val_if_fail (to_file, FALSE); + + data = g_malloc (ITUNESDB_COPYBLK); + + file_in = fopen (from_file, "r"); + if (file_in == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error opening '%s' for reading (%s)."), + from_file, g_strerror (errno)); + goto err_out; + } + + file_out = fopen (to_file, "w"); + if (file_out == NULL) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error opening '%s' for writing (%s)."), + to_file, g_strerror (errno)); + goto err_out; + } + + do { + bread = fread (data, 1, ITUNESDB_COPYBLK, file_in); +#if ITUNESDB_DEBUG + fprintf(stderr, "itunesdb_cp: read %ld bytes\n", bread); +#endif + if (bread == 0) + { + if (feof (file_in) == 0) + { /* error -- not end of file! */ + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error while reading from '%s' (%s)."), + from_file, g_strerror (errno)); + goto err_out; + } + } + else + { + bwrite = fwrite (data, 1, bread, file_out); +#if ITUNESDB_DEBUG + fprintf(stderr, "itunesdb_cp: wrote %ld bytes\n", bwrite); +#endif + if (bwrite != bread) + { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error while writing to '%s' (%s)."), + to_file, g_strerror (errno)); + goto err_out; + } + } + } while (bread != 0); + + if (fclose (file_in) != 0) + { + file_in = NULL; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error when closing '%s' (%s)."), + from_file, g_strerror (errno)); + goto err_out; + } + if (fclose (file_out) != 0) + { + file_out = NULL; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Error when closing '%s' (%s)."), + to_file, g_strerror (errno)); + goto err_out; + } + g_free (data); + return TRUE; + + err_out: + if (file_in) fclose (file_in); + if (file_out) fclose (file_out); + remove (to_file); + g_free (data); + return FALSE; +} + + + + + + +/*------------------------------------------------------------------*\ + * * + * Timestamp stuff * + * * +\*------------------------------------------------------------------*/ + +guint64 itdb_time_get_mac_time (void) +{ + GTimeVal time; + + g_get_current_time (&time); + return itdb_time_host_to_mac (time.tv_sec); +} + + +/* convert Macintosh timestamp to host system time stamp -- modify + * this function if necessary to port to host systems with different + * start of Epoch */ +/* A "0" time will not be converted */ +time_t itdb_time_mac_to_host (guint64 mactime) +{ + if (mactime != 0) return (time_t)(mactime - 2082844800); + else return (time_t)mactime; +} + + +/* convert host system timestamp to Macintosh time stamp -- modify + * this function if necessary to port to host systems with different + * start of Epoch */ +guint64 itdb_time_host_to_mac (time_t time) +{ + return (guint64)(((gint64)time) + 2082844800); +} diff --git a/src/itdb_playlist.c b/src/itdb_playlist.c new file mode 100644 index 0000000..5c68c60 --- /dev/null +++ b/src/itdb_playlist.c @@ -0,0 +1,1290 @@ +/* Time-stamp: <2005-08-27 21:51:32 jcs> +| +| Copyright (C) 2002-2005 Jorg Schuler <jcsjcs at users sourceforge net> +| Part of the gtkpod project. +| +| URL: http://www.gtkpod.org/ +| URL: http://gtkpod.sourceforge.net/ +| +| The code contained in this file is free software; you can redistribute +| it and/or modify it under the terms of the GNU Lesser General Public +| License as published by the Free Software Foundation; either version +| 2.1 of the License, or (at your option) any later version. +| +| This file 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 +| Lesser General Public License for more details. +| +| You should have received a copy of the GNU Lesser General Public +| License along with this code; if not, write to the Free Software +| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +| $Id$ +*/ + +#include "itdb_private.h" +#include <glib/gi18n-lib.h> +#include <string.h> + +/* spl_action_known(), itb_splr_get_field_type(), + * itb_splr_get_action_type() are adapted from source provided by + * Samuel "Otto" Wood (sam dot wood at gmail dot com). These part can + * also be used under a FreeBSD license. You may also contact Samuel + * for a complete copy of his original C++-classes. + * */ + +/* return TRUE if the smart playlist action @action is + known. Otherwise a warning is displayed and FALSE is returned. */ +gboolean itdb_spl_action_known (SPLAction action) +{ + gboolean result = FALSE; + + switch (action) + { + case SPLACTION_IS_INT: + case SPLACTION_IS_GREATER_THAN: + case SPLACTION_IS_NOT_GREATER_THAN: + case SPLACTION_IS_LESS_THAN: + case SPLACTION_IS_NOT_LESS_THAN: + case SPLACTION_IS_IN_THE_RANGE: + case SPLACTION_IS_NOT_IN_THE_RANGE: + case SPLACTION_IS_IN_THE_LAST: + case SPLACTION_IS_STRING: + case SPLACTION_CONTAINS: + case SPLACTION_STARTS_WITH: + case SPLACTION_DOES_NOT_START_WITH: + case SPLACTION_ENDS_WITH: + case SPLACTION_DOES_NOT_END_WITH: + case SPLACTION_IS_NOT_INT: + case SPLACTION_IS_NOT_IN_THE_LAST: + case SPLACTION_IS_NOT: + case SPLACTION_DOES_NOT_CONTAIN: + result = TRUE; + } + if (result == FALSE) + { /* New action! */ + g_warning (_("Unknown action (%d) in smart playlist will be ignored.\n"), action); + } + return result; +} + +/* return the logic type (string, int, date...) of the action field */ +SPLFieldType itdb_splr_get_field_type (const SPLRule *splr) +{ + g_return_val_if_fail (splr != NULL, splft_unknown); + + switch(splr->field) + { + case SPLFIELD_SONG_NAME: + case SPLFIELD_ALBUM: + case SPLFIELD_ARTIST: + case SPLFIELD_GENRE: + case SPLFIELD_KIND: + case SPLFIELD_COMMENT: + case SPLFIELD_COMPOSER: + case SPLFIELD_GROUPING: + return(splft_string); + case SPLFIELD_BITRATE: + case SPLFIELD_SAMPLE_RATE: + case SPLFIELD_YEAR: + case SPLFIELD_TRACKNUMBER: + case SPLFIELD_SIZE: + case SPLFIELD_PLAYCOUNT: + case SPLFIELD_DISC_NUMBER: + case SPLFIELD_BPM: + case SPLFIELD_RATING: + case SPLFIELD_TIME: /* time is the length of the track in + milliseconds */ + return(splft_int); + case SPLFIELD_COMPILATION: + return(splft_boolean); + case SPLFIELD_DATE_MODIFIED: + case SPLFIELD_DATE_ADDED: + case SPLFIELD_LAST_PLAYED: + return(splft_date); + case SPLFIELD_PLAYLIST: + return(splft_playlist); + } + return(splft_unknown); +} + + +/* return the type (range, date, string...) of the action field */ +SPLActionType itdb_splr_get_action_type (const SPLRule *splr) +{ + SPLFieldType fieldType; + + g_return_val_if_fail (splr != NULL, splft_unknown); + + fieldType = itdb_splr_get_field_type (splr); + + switch(fieldType) + { + case splft_string: + switch (splr->action) + { + case SPLACTION_IS_STRING: + case SPLACTION_IS_NOT: + case SPLACTION_CONTAINS: + case SPLACTION_DOES_NOT_CONTAIN: + case SPLACTION_STARTS_WITH: + case SPLACTION_DOES_NOT_START_WITH: + case SPLACTION_ENDS_WITH: + case SPLACTION_DOES_NOT_END_WITH: + return splat_string; + case SPLACTION_IS_NOT_IN_THE_RANGE: + case SPLACTION_IS_INT: + case SPLACTION_IS_NOT_INT: + case SPLACTION_IS_GREATER_THAN: + case SPLACTION_IS_NOT_GREATER_THAN: + case SPLACTION_IS_LESS_THAN: + case SPLACTION_IS_NOT_LESS_THAN: + case SPLACTION_IS_IN_THE_RANGE: + case SPLACTION_IS_IN_THE_LAST: + case SPLACTION_IS_NOT_IN_THE_LAST: + return splat_invalid; + default: + /* Unknown action type */ + g_warning ("Unknown action type %d\n\n", splr->action); + return splat_unknown; + } + break; + + case splft_int: + switch (splr->action) + { + case SPLACTION_IS_INT: + case SPLACTION_IS_NOT_INT: + case SPLACTION_IS_GREATER_THAN: + case SPLACTION_IS_NOT_GREATER_THAN: + case SPLACTION_IS_LESS_THAN: + case SPLACTION_IS_NOT_LESS_THAN: + return splat_int; + case SPLACTION_IS_NOT_IN_THE_RANGE: + case SPLACTION_IS_IN_THE_RANGE: + return splat_range_int; + case SPLACTION_IS_STRING: + case SPLACTION_CONTAINS: + case SPLACTION_STARTS_WITH: + case SPLACTION_DOES_NOT_START_WITH: + case SPLACTION_ENDS_WITH: + case SPLACTION_DOES_NOT_END_WITH: + case SPLACTION_IS_IN_THE_LAST: + case SPLACTION_IS_NOT_IN_THE_LAST: + case SPLACTION_IS_NOT: + case SPLACTION_DOES_NOT_CONTAIN: + return splat_invalid; + default: + /* Unknown action type */ + g_warning ("Unknown action type %d\n\n", splr->action); + return splat_unknown; + } + break; + + case splft_boolean: + return splat_none; + + case splft_date: + switch (splr->action) + { + case SPLACTION_IS_INT: + case SPLACTION_IS_NOT_INT: + case SPLACTION_IS_GREATER_THAN: + case SPLACTION_IS_NOT_GREATER_THAN: + case SPLACTION_IS_LESS_THAN: + case SPLACTION_IS_NOT_LESS_THAN: + return splat_date; + case SPLACTION_IS_IN_THE_LAST: + case SPLACTION_IS_NOT_IN_THE_LAST: + return splat_inthelast; + case SPLACTION_IS_IN_THE_RANGE: + case SPLACTION_IS_NOT_IN_THE_RANGE: + return splat_range_date; + case SPLACTION_IS_STRING: + case SPLACTION_CONTAINS: + case SPLACTION_STARTS_WITH: + case SPLACTION_DOES_NOT_START_WITH: + case SPLACTION_ENDS_WITH: + case SPLACTION_DOES_NOT_END_WITH: + case SPLACTION_IS_NOT: + case SPLACTION_DOES_NOT_CONTAIN: + return splat_invalid; + default: + /* Unknown action type */ + g_warning ("Unknown action type %d\n\n", splr->action); + return splat_unknown; + } + break; + + case splft_playlist: + switch (splr->action) + { + case SPLACTION_IS_INT: + case SPLACTION_IS_NOT_INT: + return splat_playlist; + case SPLACTION_IS_GREATER_THAN: + case SPLACTION_IS_NOT_GREATER_THAN: + case SPLACTION_IS_LESS_THAN: + case SPLACTION_IS_NOT_LESS_THAN: + case SPLACTION_IS_IN_THE_LAST: + case SPLACTION_IS_NOT_IN_THE_LAST: + case SPLACTION_IS_IN_THE_RANGE: + case SPLACTION_IS_NOT_IN_THE_RANGE: + case SPLACTION_IS_STRING: + case SPLACTION_CONTAINS: + case SPLACTION_STARTS_WITH: + case SPLACTION_DOES_NOT_START_WITH: + case SPLACTION_ENDS_WITH: + case SPLACTION_DOES_NOT_END_WITH: + case SPLACTION_IS_NOT: + case SPLACTION_DOES_NOT_CONTAIN: + return splat_invalid; + default: + /* Unknown action type */ + g_warning ("Unknown action type %d\n\n", splr->action); + return splat_unknown; + } + + case splft_unknown: + /* Unknown action type */ + g_warning ("Unknown action type %d\n\n", splr->action); + return splat_unknown; + } + return splat_unknown; +} + +/* ------------------------------------------------------------------- + * + * smart playlist stuff, adapted from source provided by Samuel "Otto" + * Wood (sam dot wood at gmail dot com). This part can also be used + * under a FreeBSD license. You can also contact Samuel for a complete + * copy of his original C++-classes. + * + */ + + +/* function to evaluate a rule's truth against a track */ +gboolean itdb_splr_eval (Itdb_iTunesDB *itdb, SPLRule *splr, Itdb_Track *track) +{ + SPLFieldType ft; + SPLActionType at; + gchar *strcomp = NULL; + gint64 intcomp = 0; + gboolean boolcomp = FALSE; + guint32 datecomp = 0; + Itdb_Playlist *playcomp = NULL; + time_t t; + guint64 mactime; + + g_return_val_if_fail (splr != NULL, FALSE); + g_return_val_if_fail (track != NULL, FALSE); + + ft = itdb_splr_get_field_type (splr); + at = itdb_splr_get_action_type (splr); + + g_return_val_if_fail (at != splat_invalid, FALSE); + + /* find what we need to compare in the track */ + switch (splr->field) + { + case SPLFIELD_SONG_NAME: + strcomp = track->title; + break; + case SPLFIELD_ALBUM: + strcomp = track->album; + break; + case SPLFIELD_ARTIST: + strcomp = track->artist; + break; + case SPLFIELD_GENRE: + strcomp = track->genre; + break; + case SPLFIELD_KIND: + strcomp = track->fdesc; + break; + case SPLFIELD_COMMENT: + strcomp = track->comment; + break; + case SPLFIELD_COMPOSER: + strcomp = track->composer; + break; + case SPLFIELD_GROUPING: + strcomp = track->grouping; + break; + case SPLFIELD_BITRATE: + intcomp = track->bitrate; + break; + case SPLFIELD_SAMPLE_RATE: + intcomp = track->samplerate; + break; + case SPLFIELD_YEAR: + intcomp = track->year; + break; + case SPLFIELD_TRACKNUMBER: + intcomp = track->track_nr; + break; + case SPLFIELD_SIZE: + intcomp = track->size; + break; + case SPLFIELD_PLAYCOUNT: + intcomp = track->playcount; + break; + case SPLFIELD_DISC_NUMBER: + intcomp = track->cd_nr; + break; + case SPLFIELD_BPM: + intcomp = track->BPM; + break; + case SPLFIELD_RATING: + intcomp = track->rating; + break; + case SPLFIELD_TIME: + intcomp = track->tracklen/1000; + break; + case SPLFIELD_COMPILATION: + boolcomp = track->compilation; + break; + case SPLFIELD_DATE_MODIFIED: + datecomp = track->time_modified; + break; + case SPLFIELD_DATE_ADDED: + datecomp = track->time_added; + break; + case SPLFIELD_LAST_PLAYED: + datecomp = track->time_played; + break; + case SPLFIELD_PLAYLIST: + playcomp = itdb_playlist_by_id (itdb, splr->fromvalue); + break; + default: /* unknown field type */ + g_return_val_if_fail (FALSE, FALSE); + } + + /* actually do the comparison to our rule */ + switch (ft) + { + case splft_string: + if(strcomp && splr->string) + { + gint len1 = strlen (strcomp); + gint len2 = strlen (splr->string); + switch (splr->action) + { + case SPLACTION_IS_STRING: + return (strcmp (strcomp, splr->string) == 0); + case SPLACTION_IS_NOT: + return (strcmp (strcomp, splr->string) != 0); + case SPLACTION_CONTAINS: + return (strstr (strcomp, splr->string) != NULL); + case SPLACTION_DOES_NOT_CONTAIN: + return (strstr (strcomp, splr->string) == NULL); + case SPLACTION_STARTS_WITH: + return (strncmp (strcomp, splr->string, len2) == 0); + case SPLACTION_ENDS_WITH: + if (len2 > len1) return FALSE; + return (strncmp (strcomp+len1-len2, + splr->string, len2) == 0); + case SPLACTION_DOES_NOT_START_WITH: + return (strncmp (strcomp, splr->string, + strlen (splr->string)) != 0); + case SPLACTION_DOES_NOT_END_WITH: + if (len2 > len1) return TRUE; + return (strncmp (strcomp+len1-len2, + splr->string, len2) != 0); + }; + } + return FALSE; + case splft_int: + switch(splr->action) + { + case SPLACTION_IS_INT: + return (intcomp == splr->fromvalue); + case SPLACTION_IS_NOT_INT: + return (intcomp != splr->fromvalue); + case SPLACTION_IS_GREATER_THAN: + return (intcomp > splr->fromvalue); + case SPLACTION_IS_LESS_THAN: + return (intcomp < splr->fromvalue); + case SPLACTION_IS_IN_THE_RANGE: + return ((intcomp <= splr->fromvalue && + intcomp >= splr->tovalue) || + (intcomp >= splr->fromvalue && + intcomp <= splr->tovalue)); + case SPLACTION_IS_NOT_IN_THE_RANGE: + return ((intcomp < splr->fromvalue && + intcomp < splr->tovalue) || + (intcomp > splr->fromvalue && + intcomp > splr->tovalue)); + } + return FALSE; + case splft_boolean: + switch (splr->action) + { + case SPLACTION_IS_INT: /* aka "is set" */ + return (boolcomp != 0); + case SPLACTION_IS_NOT_INT: /* aka "is not set" */ + return (boolcomp == 0); + } + return FALSE; + case splft_date: + switch (splr->action) + { + case SPLACTION_IS_INT: + return (datecomp == splr->fromvalue); + case SPLACTION_IS_NOT_INT: + return (datecomp != splr->fromvalue); + case SPLACTION_IS_GREATER_THAN: + return (datecomp > splr->fromvalue); + case SPLACTION_IS_LESS_THAN: + return (datecomp < splr->fromvalue); + case SPLACTION_IS_NOT_GREATER_THAN: + return (datecomp <= splr->fromvalue); + case SPLACTION_IS_NOT_LESS_THAN: + return (datecomp >= splr->fromvalue); + case SPLACTION_IS_IN_THE_LAST: + time (&t); + t += (splr->fromdate * splr->fromunits); + mactime = itdb_time_host_to_mac (t); + return (datecomp > mactime); + case SPLACTION_IS_NOT_IN_THE_LAST: + time (&t); + t += (splr->fromdate * splr->fromunits); + mactime = itdb_time_host_to_mac (t); + return (datecomp <= mactime); + case SPLACTION_IS_IN_THE_RANGE: + return ((datecomp <= splr->fromvalue && + datecomp >= splr->tovalue) || + (datecomp >= splr->fromvalue && + datecomp <= splr->tovalue)); + case SPLACTION_IS_NOT_IN_THE_RANGE: + return ((datecomp < splr->fromvalue && + datecomp < splr->tovalue) || + (datecomp > splr->fromvalue && + datecomp > splr->tovalue)); + } + return FALSE; + case splft_playlist: + /* if we didn't find the playlist, just exit instead of + dealing with it */ + if (playcomp == NULL) return FALSE; + + switch(splr->action) + { + case SPLACTION_IS_INT: /* is this track in this playlist? */ + return (itdb_playlist_contains_track (playcomp, track)); + case SPLACTION_IS_NOT_INT:/* NOT in this playlist? */ + return (!itdb_playlist_contains_track (playcomp, track)); + } + return FALSE; + case splft_unknown: + g_return_val_if_fail (ft != splft_unknown, FALSE); + return FALSE; + default: /* new type: warning to change this code */ + g_return_val_if_fail (FALSE, FALSE); + return FALSE; + } + /* we should never make it out of the above switches alive */ + g_return_val_if_fail (FALSE, FALSE); + return FALSE; +} + +/* local functions to help with the sorting of the list of tracks so + * that we can do limits */ +static gint compTitle (Itdb_Track *a, Itdb_Track *b) +{ + return strcmp (a->title, b->title); +} +static gint compAlbum (Itdb_Track *a, Itdb_Track *b) +{ + return strcmp (a->album, b->album); +} +static gint compArtist (Itdb_Track *a, Itdb_Track *b) +{ + return strcmp (a->artist, b->artist); +} +static gint compGenre (Itdb_Track *a, Itdb_Track *b) +{ + return strcmp (a->genre, b->genre); +} +static gint compMostRecentlyAdded (Itdb_Track *a, Itdb_Track *b) +{ + return b->time_added - a->time_added; +} +static gint compLeastRecentlyAdded (Itdb_Track *a, Itdb_Track *b) +{ + return a->time_added - b->time_added; +} +static gint compMostOftenPlayed (Itdb_Track *a, Itdb_Track *b) +{ + return b->time_added - a->time_added; +} +static gint compLeastOftenPlayed (Itdb_Track *a, Itdb_Track *b) +{ + return a->time_added - b->time_added; +} +static gint compMostRecentlyPlayed (Itdb_Track *a, Itdb_Track *b) +{ + return b->time_played - a->time_played; +} +static gint compLeastRecentlyPlayed (Itdb_Track *a, Itdb_Track *b) +{ + return a->time_played - b->time_played; +} +static gint compHighestRating (Itdb_Track *a, Itdb_Track *b) +{ + return b->rating - a->rating; +} +static gint compLowestRating (Itdb_Track *a, Itdb_Track *b) +{ + return a->rating - b->rating; +} + +/* Randomize the order of the members of the GList @list */ +/* Returns a pointer to the new start of the list */ +static GList *randomize_glist (GList *list) +{ + gint32 nr = g_list_length (list); + + while (nr > 1) + { + /* get random element among the first nr members */ + gint32 rand = g_random_int_range (0, nr); + GList *gl = g_list_nth (list, rand); + /* remove it and add it at the end */ + list = g_list_remove_link (list, gl); + list = g_list_concat (list, gl); + --nr; + } + return list; +} + + +/* Randomizes a playlist */ +void itdb_playlist_randomize (Itdb_Playlist *pl) +{ + g_return_if_fail (pl); + + pl->members = randomize_glist (pl->members); +} + + +void itdb_spl_update (Itdb_iTunesDB *itdb, Itdb_Playlist *spl) +{ + GList *gl; + GList *sel_tracks = NULL; + + g_return_if_fail (spl); + g_return_if_fail (itdb); + + /* we only can populate smart playlists */ + if (!spl->is_spl) return; + + /* clear this playlist */ + g_list_free (spl->members); + spl->members = NULL; + spl->num = 0; + + for (gl=itdb->tracks; gl ; gl=gl->next) + { + Itdb_Track *t = gl->data; + g_return_if_fail (t); + /* skip non-checked songs if we have to do so (this takes care + of *all* the match_checked functionality) */ + if (spl->splpref.matchcheckedonly && (t->checked == 0)) + continue; + /* first, match the rules */ + if (spl->splpref.checkrules) + { /* if we are set to check the rules */ + /* start with true for "match all", + start with false for "match any" */ + gboolean matchrules; + GList *gl; + + if (spl->splrules.match_operator == SPLMATCH_AND) + matchrules = TRUE; + else matchrules = FALSE; + /* assume everything matches with no rules */ + if (spl->splrules.rules == NULL) matchrules = TRUE; + /* match all rules */ + for (gl=spl->splrules.rules; gl; gl=gl->next) + { + SPLRule* splr = gl->data; + gboolean ruletruth = itdb_splr_eval (itdb, splr, t); + if (spl->splrules.match_operator == SPLMATCH_AND) + { + if (!ruletruth) + { /* one rule did not match -- we can stop */ + matchrules = FALSE; + break; + } + } + else if (spl->splrules.match_operator == SPLMATCH_OR) + { + if (ruletruth) + { /* one rule matched -- we can stop */ + matchrules = TRUE; + break; + } + } + } + if (matchrules) + { /* we have a track that matches the ruleset, append to + * playlist for now*/ + sel_tracks = g_list_append (sel_tracks, t); + } + } + else + { /* we aren't checking the rules, so just append to + playlist */ + sel_tracks = g_list_append (sel_tracks, t); + } + } + /* no reason to go on if nothing matches so far */ + if (g_list_length (sel_tracks) == 0) return; + + /* do the limits */ + if (spl->splpref.checklimits) + { + /* use a double because we may need to deal with fractions + * here */ + gdouble runningtotal = 0; + guint32 trackcounter = 0; + guint32 tracknum = g_list_length (sel_tracks); + +/* printf("limitsort: %d\n", spl->splpref.limitsort); */ + + /* limit to (number) (type) selected by (sort) */ + /* first, we sort the list */ + switch(spl->splpref.limitsort) + { + case LIMITSORT_RANDOM: + sel_tracks = randomize_glist (sel_tracks); + break; + case LIMITSORT_SONG_NAME: + sel_tracks = g_list_sort (sel_tracks, (GCompareFunc)compTitle); + break; + case LIMITSORT_ALBUM: + sel_tracks = g_list_sort (sel_tracks, (GCompareFunc)compAlbum); + break; + case LIMITSORT_ARTIST: + sel_tracks = g_list_sort (sel_tracks, (GCompareFunc)compArtist); + break; + case LIMITSORT_GENRE: + sel_tracks = g_list_sort (sel_tracks, (GCompareFunc)compGenre); + break; + case LIMITSORT_MOST_RECENTLY_ADDED: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compMostRecentlyAdded); + break; + case LIMITSORT_LEAST_RECENTLY_ADDED: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compLeastRecentlyAdded); + break; + case LIMITSORT_MOST_OFTEN_PLAYED: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compMostOftenPlayed); + break; + case LIMITSORT_LEAST_OFTEN_PLAYED: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compLeastOftenPlayed); + break; + case LIMITSORT_MOST_RECENTLY_PLAYED: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compMostRecentlyPlayed); + break; + case LIMITSORT_LEAST_RECENTLY_PLAYED: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compLeastRecentlyPlayed); + break; + case LIMITSORT_HIGHEST_RATING: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compHighestRating); + break; + case LIMITSORT_LOWEST_RATING: + sel_tracks = g_list_sort (sel_tracks, + (GCompareFunc)compLowestRating); + break; + default: + g_warning ("Programming error: should not reach this point (default of switch (spl->splpref.limitsort)\n"); + break; + } + /* now that the list is sorted in the order we want, we + take the top X tracks off the list and insert them into + our playlist */ + + while ((runningtotal < spl->splpref.limitvalue) && + (trackcounter < tracknum)) + { + gdouble currentvalue=0; + Itdb_Track *t = g_list_nth_data (sel_tracks, trackcounter); + +/* printf ("track: %d runningtotal: %lf, limitvalue: %d\n", */ +/* trackcounter, runningtotal, spl->splpref.limitvalue); */ + + /* get the next song's value to add to running total */ + switch (spl->splpref.limittype) + { + case LIMITTYPE_MINUTES: + currentvalue = (double)(t->tracklen)/(60*1000); + break; + case LIMITTYPE_HOURS: + currentvalue = (double)(t->tracklen)/(60*60*1000); + break; + case LIMITTYPE_MB: + currentvalue = (double)(t->size)/(1024*1024); + break; + case LIMITTYPE_GB: + currentvalue = (double)(t->size)/(1024*1024*1024); + break; + case LIMITTYPE_SONGS: + currentvalue = 1; + break; + default: + g_warning ("Programming error: should not reach this point (default of switch (spl->splpref.limittype)\n"); + break; + } + /* check to see that we won't actually exceed the + * limitvalue */ + if (runningtotal + currentvalue <= + spl->splpref.limitvalue) + { + runningtotal += currentvalue; + /* Add the playlist entry */ + itdb_playlist_add_track (spl, t, -1); + } + /* increment the track counter so we can look at the next + track */ + trackcounter++; +/* printf (" track: %d runningtotal: %lf, limitvalue: %d\n", */ +/* trackcounter, runningtotal, spl->splpref.limitvalue); */ + } /* end while */ + /* no longer needed */ + g_list_free (sel_tracks); + sel_tracks = NULL; + } /* end if limits enabled */ + else + { /* no limits, so stick everything that matched the rules into + the playlist */ + spl->members = sel_tracks; + spl->num = g_list_length (sel_tracks); + sel_tracks = NULL; + } +} + + + + +/* update all smart playlists */ +void itdb_spl_update_all (Itdb_iTunesDB *itdb) +{ + void spl_update (Itdb_Playlist *playlist, Itdb_iTunesDB *itdb) + { + g_return_if_fail (playlist); + itdb_spl_update (itdb, playlist); + } + + g_return_if_fail (itdb); + + g_list_foreach (itdb->playlists, (GFunc)spl_update, itdb); +} + + + +/* end of code based on Samuel Wood's work */ +/* ------------------------------------------------------------------- */ + + +/* Validate a rule */ +void itdb_splr_validate (SPLRule *splr) +{ + SPLActionType at; + + g_return_if_fail (splr != NULL); + + at = itdb_splr_get_action_type (splr); + + g_return_if_fail (at != splat_unknown); + + switch (at) + { + case splat_int: + case splat_playlist: + case splat_date: + splr->fromdate = 0; + splr->fromunits = 1; + splr->tovalue = splr->fromvalue; + splr->todate = 0; + splr->tounits = 1; + break; + case splat_range_int: + case splat_range_date: + splr->fromdate = 0; + splr->fromunits = 1; + splr->todate = 0; + splr->tounits = 1; + break; + case splat_inthelast: + splr->fromvalue = SPLDATE_IDENTIFIER; + splr->tovalue = SPLDATE_IDENTIFIER; + break; + case splat_none: + case splat_string: + splr->fromvalue = 0; + splr->fromdate = 0; + splr->fromunits = 0; + splr->tovalue = 0; + splr->todate = 0; + splr->tounits = 0; + break; + case splat_invalid: + case splat_unknown: + g_return_if_fail (FALSE); + break; + } + +} + + +/* Free memory of SPLRule @splr */ +static void itdb_splr_free (SPLRule *splr) +{ + if (splr) + { + g_free (splr->string); + g_free (splr); + } +} + +/* remove @splr from playlist @pl */ +void itdb_splr_remove (Itdb_Playlist *pl, SPLRule *splr) +{ + g_return_if_fail (pl); + g_return_if_fail (splr); + + pl->splrules.rules = g_list_remove (pl->splrules.rules, splr); + itdb_splr_free (splr); +} + +/* add smart rule @splr to playlist @pl at position @pos */ +void itdb_splr_add (Itdb_Playlist *pl, SPLRule *splr, gint pos) +{ + g_return_if_fail (pl); + g_return_if_fail (splr); + + pl->splrules.rules = g_list_insert (pl->splrules.rules, + splr, pos); +} + + +/* Create new default rule */ +SPLRule *itdb_splr_new (void) +{ + SPLRule *splr = g_new0 (SPLRule, 1); + + splr->field = SPLFIELD_ARTIST; + splr->action = SPLACTION_CONTAINS; + splr->fromvalue = 0; + splr->fromdate = 0; + splr->fromunits = 0; + splr->tovalue = 0; + splr->todate = 0; + splr->tounits = 0; + + return splr; +} + + +/* create a new smart rule and insert it at position @pos of playlist + * @pl. A pointer to the newly created rule is returned. */ +SPLRule *itdb_splr_add_new (Itdb_Playlist *pl, gint pos) +{ + SPLRule *splr; + + g_return_val_if_fail (pl, NULL); + + splr = itdb_splr_new (); + itdb_splr_add (pl, splr, pos); + return splr; +} + +/* Duplicate SPLRule @splr */ +static SPLRule *splr_duplicate (SPLRule *splr) +{ + SPLRule *dup = NULL; + if (splr) + { + dup = g_malloc (sizeof (SPLRule)); + memcpy (dup, splr, sizeof (SPLRule)); + + /* Now copy the strings */ + dup->string = g_strdup (splr->string); + } + return dup; +} + + +/* Duplicate an existing playlist. pl_dup->id is set to zero, so that + * it will be set to a unique value when adding it to the itdb. */ +Itdb_Playlist *itdb_playlist_duplicate (Itdb_Playlist *pl) +{ + Itdb_Playlist *pl_dup; + GList *gl; + + g_return_val_if_fail (pl, NULL); + g_return_val_if_fail (!pl->userdata || pl->userdata_duplicate, NULL); + + pl_dup = g_new0 (Itdb_Playlist, 1); + memcpy (pl_dup, pl, sizeof (Itdb_Playlist)); + /* clear list heads */ + pl_dup->members = NULL; + pl_dup->splrules.rules = NULL; + + /* clear itdb pointer */ + pl_dup->itdb = NULL; + + /* Now copy strings */ + pl_dup->name = g_strdup (pl->name); + + /* Copy members */ + pl_dup->members = g_list_copy (pl->members); + + /* Copy rules */ + for (gl=pl->splrules.rules; gl; gl=gl->next) + { + SPLRule *splr_dup = splr_duplicate (gl->data); + pl_dup->splrules.rules = g_list_append ( + pl_dup->splrules.rules, splr_dup); + } + + /* Set id to 0, so it will be set to a unique value when adding + * this playlist to a itdb */ + pl_dup->id = 0; + + /* Copy userdata */ + if (pl->userdata) + pl_dup->userdata = pl->userdata_duplicate (pl->userdata); + + return pl_dup; +} + + +/* copy all relevant information for smart playlist from playlist @src + to playlist @dest. Already available information is + overwritten/deleted. */ +void itdb_spl_copy_rules (Itdb_Playlist *dest, Itdb_Playlist *src) +{ + GList *gl; + + g_return_if_fail (dest); + g_return_if_fail (src); + g_return_if_fail (dest->is_spl); + g_return_if_fail (src->is_spl); + + /* remove existing rules */ + g_list_foreach (dest->splrules.rules, (GFunc)(itdb_splr_free), NULL); + g_list_free (dest->splrules.rules); + + /* copy general spl settings */ + memcpy (&dest->splpref, &src->splpref, sizeof (SPLPref)); + memcpy (&dest->splrules, &src->splrules, sizeof (SPLRules)); + dest->splrules.rules = NULL; + + /* Copy rules */ + for (gl=src->splrules.rules; gl; gl=gl->next) + { + SPLRule *splr_dup = splr_duplicate (gl->data); + dest->splrules.rules = g_list_append ( + dest->splrules.rules, splr_dup); + } +} + + + +/* Generate a new playlist structure. If @spl is TRUE, a smart + * playlist is generated. pl->id is set when adding to an itdb by + * itdb_playlist_add()*/ +Itdb_Playlist *itdb_playlist_new (const gchar *title, gboolean spl) +{ + Itdb_Playlist *pl = g_new0 (Itdb_Playlist, 1); + + pl->type = ITDB_PL_TYPE_NORM; + pl->name = g_strdup (title); + pl->is_spl = spl; + if (spl) + { + pl->splpref.liveupdate = TRUE; + pl->splpref.checkrules = TRUE; + pl->splpref.checklimits = FALSE; + pl->splpref.limittype = LIMITTYPE_HOURS; + pl->splpref.limitsort = LIMITSORT_RANDOM; + pl->splpref.limitvalue = 2; + pl->splpref.matchcheckedonly = FALSE; + pl->splrules.match_operator = SPLMATCH_AND; + /* add at least one rule */ + itdb_splr_add_new (pl, 0); + } + return pl; +} + + +/* Free the memory taken by playlist @pl. */ +void itdb_playlist_free (Itdb_Playlist *pl) +{ + g_return_if_fail (pl); + + g_free (pl->name); + g_list_free (pl->members); + g_list_foreach (pl->splrules.rules, + (GFunc)(itdb_splr_free), NULL); + g_list_free (pl->splrules.rules); + if (pl->userdata && pl->userdata_destroy) + (*pl->userdata_destroy) (pl->userdata); + g_free (pl); +} + + + +/* add playlist @pl to the database @itdb at position @pos (-1 for + * "append to end"). A unique id is created if pl->id is equal to + * zero. */ +/* a critical message is logged if either itdb or pl is NULL */ +void itdb_playlist_add (Itdb_iTunesDB *itdb, Itdb_Playlist *pl, gint32 pos) +{ + g_return_if_fail (itdb); + g_return_if_fail (pl); + g_return_if_fail (!pl->userdata || pl->userdata_duplicate); + + pl->itdb = itdb; + + /* set unique ID when not yet set */ + if (pl->id == 0) + { + GRand *grand = g_rand_new (); + GList *gl; + guint64 id; + do + { + id = ((guint64)g_rand_int (grand) << 32) | + ((guint64)g_rand_int (grand)); + /* check if id is really random (with 100 playlists the + * chance to create a duplicate is 1 in + * 184,467,440,737,095,516.16) */ + for (gl=itdb->playlists; id && gl; gl=gl->next) + { + Itdb_Playlist *g_pl = gl->data; + g_return_if_fail (g_pl); + if (id == g_pl->id) id = 0; + } + } while (id == 0); + pl->id = id; + } + if (pos == -1) itdb->playlists = g_list_append (itdb->playlists, pl); + else itdb->playlists = g_list_insert (itdb->playlists, pl, pos); +} + + + +/* move playlist @pl to position @pos */ +void itdb_playlist_move (Itdb_Playlist *pl, guint32 pos) +{ + Itdb_iTunesDB *itdb; + + g_return_if_fail (pl); + itdb = pl->itdb; + g_return_if_fail (itdb); + + itdb->playlists = g_list_remove (itdb->playlists, pl); + itdb->playlists = g_list_insert (itdb->playlists, pl, pos); +} + + +/* Remove playlist @pl and free memory */ +void itdb_playlist_remove (Itdb_Playlist *pl) +{ + Itdb_iTunesDB *itdb; + + g_return_if_fail (pl); + itdb = pl->itdb; + g_return_if_fail (itdb); + + itdb->playlists = g_list_remove (itdb->playlists, pl); + itdb_playlist_free (pl); +} + + +/* Remove playlist @pl but do not free memory */ +/* pl->itdb is set to NULL */ +void itdb_playlist_unlink (Itdb_Playlist *pl) +{ + Itdb_iTunesDB *itdb; + + g_return_if_fail (pl); + itdb = pl->itdb; + g_return_if_fail (itdb); + + itdb->playlists = g_list_remove (itdb->playlists, pl); + pl->itdb = NULL; +} + + +/* Return TRUE if the playlist @pl exists, FALSE otherwise */ +gboolean itdb_playlist_exists (Itdb_iTunesDB *itdb, Itdb_Playlist *pl) +{ + g_return_val_if_fail (itdb, FALSE); + g_return_val_if_fail (pl, FALSE); + + if (g_list_find (itdb->playlists, pl)) return TRUE; + else return FALSE; +} + + +/* add @track to playlist @pl position @pos (-1 for "append to + * end") */ +/* a critical message is logged if either @itdb, @pl or @track is + NULL */ +void itdb_playlist_add_track (Itdb_Playlist *pl, + Itdb_Track *track, gint32 pos) +{ + g_return_if_fail (pl); + g_return_if_fail (pl->itdb); + g_return_if_fail (track); + + track->itdb = pl->itdb; + + if (pos == -1) pl->members = g_list_append (pl->members, track); + else pl->members = g_list_insert (pl->members, track, pos); +} + + + +/* Remove track @track from playlist *pl. If @pl == NULL remove from + * master playlist. */ +void itdb_playlist_remove_track (Itdb_Playlist *pl, Itdb_Track *track) +{ + g_return_if_fail (track); + + if (pl == NULL) + pl = itdb_playlist_mpl (track->itdb); + + g_return_if_fail (pl); + + pl->members = g_list_remove (pl->members, track); +} + + +/* Returns the playlist with the ID @id or NULL if the ID cannot be + * found. */ +Itdb_Playlist *itdb_playlist_by_id (Itdb_iTunesDB *itdb, guint64 id) +{ + GList *gl; + + g_return_val_if_fail (itdb, NULL); + + for (gl=itdb->playlists; gl; gl=gl->next) + { + Itdb_Playlist *pl = gl->data; + if (pl->id == id) return pl; + } + return NULL; +} + + +/* Return playlist at position @num in @itdb */ +Itdb_Playlist *itdb_playlist_by_nr (Itdb_iTunesDB *itdb, guint32 num) +{ + Itdb_Playlist *pl; + g_return_val_if_fail (itdb, NULL); + pl = g_list_nth_data (itdb->playlists, num); + g_return_val_if_fail (pl, NULL); + return pl; +} + + +/* Return first playlist with name @name. */ +Itdb_Playlist *itdb_playlist_by_name (Itdb_iTunesDB *itdb, gchar *name) +{ + GList *gl; + g_return_val_if_fail (itdb, NULL); + g_return_val_if_fail (name, NULL); + + for (gl=itdb->playlists; gl; gl=gl->next) + { + Itdb_Playlist *pl = gl->data; + g_return_val_if_fail (pl, NULL); + if (pl->name && (strcmp (pl->name, name) == 0)) + return pl; + } + return NULL; +} + + +/* return the master playlist of @itdb */ +Itdb_Playlist *itdb_playlist_mpl (Itdb_iTunesDB *itdb) +{ + Itdb_Playlist *pl; + + g_return_val_if_fail (itdb, NULL); + + pl = g_list_nth_data (itdb->playlists, 0); + g_return_val_if_fail (pl, NULL); + + /* mpl is guaranteed to be at first position... */ + g_return_val_if_fail (pl->type == ITDB_PL_TYPE_MPL, NULL); + + return pl; +} + + +/* checks if @track is in playlist @pl. TRUE, if yes, FALSE + otherwise. If @pl is NULL, the */ +gboolean itdb_playlist_contains_track (Itdb_Playlist *pl, Itdb_Track *tr) +{ + g_return_val_if_fail (tr, FALSE); + + if (pl == NULL) + pl = itdb_playlist_mpl (tr->itdb); + + g_return_val_if_fail (pl, FALSE); + + if (g_list_find (pl->members, tr)) return TRUE; + else return FALSE; +} + + +/* returns in how many playlists (other than the MPL) @track is a + member of */ +guint32 itdb_playlist_contain_track_number (Itdb_Track *tr) +{ + Itdb_iTunesDB *itdb; + guint32 num = 0; + GList *gl; + + g_return_val_if_fail (tr, 0); + itdb = tr->itdb; + g_return_val_if_fail (itdb, 0); + + /* start with 2nd playlist (skip MPL) */ + gl = g_list_nth (itdb->playlists, 1); + while (gl) + { + g_return_val_if_fail (gl->data, num); + if (itdb_playlist_contains_track (gl->data, tr)) ++num; + gl = gl->next; + } + return num; +} + + + +/* return number of tracks in playlist */ +guint32 itdb_playlist_tracks_number (Itdb_Playlist *pl) +{ + g_return_val_if_fail (pl, 0); + + return g_list_length (pl->members); +} diff --git a/src/itdb_private.h b/src/itdb_private.h new file mode 100644 index 0000000..72e8558 --- /dev/null +++ b/src/itdb_private.h @@ -0,0 +1,100 @@ +/* Time-stamp: <2005-07-09 15:48:29 jcs> +| +| Copyright (C) 2002-2005 Jorg Schuler <jcsjcs at users sourceforge net> +| Part of the gtkpod project. +| +| URL: http://www.gtkpod.org/ +| URL: http://gtkpod.sourceforge.net/ +| +| The code contained in this file is free software; you can redistribute +| it and/or modify it under the terms of the GNU Lesser General Public +| License as published by the Free Software Foundation; either version +| 2.1 of the License, or (at your option) any later version. +| +| This file 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 +| Lesser General Public License for more details. +| +| You should have received a copy of the GNU Lesser General Public +| License along with this code; if not, write to the Free Software +| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +| $Id$ +*/ + +#ifndef __ITDB_PRIVATE_H__ +#define __ITDB_PRIVATE_H__ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "itdb.h" + +/* keeps the contents of one disk file (read) */ +typedef struct +{ + gchar *filename; + gchar *contents; + gsize length; + GError *error; +} FContents; + + +/* struct used to hold all necessary information when importing a + Itdb_iTunesDB */ +typedef struct +{ + Itdb_iTunesDB *itdb; + FContents *itunesdb; + GList *pos_glist; /* temporary list to store position indicators */ + GList *playcounts; /* contents of Play Counts file */ + GTree *idtree; /* temporary tree with track id tree */ + GError *error; /* where to report errors to */ +} FImport; + +/* data of playcounts GList above */ +struct playcount { + guint32 playcount; + guint32 time_played; + guint32 bookmark_time; + gint32 rating; + gint32 unk16; +}; + +/* value to indicate that playcount was not set in struct playcount + above */ +#define NO_PLAYCOUNT (-1) + + +/* keeps the contents of the output file (write) */ +typedef struct +{ + gchar *filename; + gchar *contents; /* pointer to contents */ + gulong pos; /* current write position ("end of file") */ + gulong total; /* current total size of *contents array */ + GError *error; /* place to report errors to */ +} WContents; + +/* size of memory by which the total size of above WContents gets + * increased (1.5 MB) */ +#define WCONTENTS_STEPSIZE 1572864 + +/* struct used to hold all necessary information when exporting a + * Itdb_iTunesDB */ +typedef struct +{ + Itdb_iTunesDB *itdb; + WContents *itunesdb; + GError *error; /* where to report errors to */ +} FExport; + + +gboolean itdb_spl_action_known (SPLAction action); +#endif diff --git a/src/itdb_track.c b/src/itdb_track.c new file mode 100644 index 0000000..bad9f8e --- /dev/null +++ b/src/itdb_track.c @@ -0,0 +1,205 @@ +/* Time-stamp: <2005-06-17 22:12:16 jcs> +| +| Copyright (C) 2002-2005 Jorg Schuler <jcsjcs at users sourceforge net> +| Part of the gtkpod project. +| +| URL: http://www.gtkpod.org/ +| URL: http://gtkpod.sourceforge.net/ +| +| The code contained in this file is free software; you can redistribute +| it and/or modify it under the terms of the GNU Lesser General Public +| License as published by the Free Software Foundation; either version +| 2.1 of the License, or (at your option) any later version. +| +| This file 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 +| Lesser General Public License for more details. +| +| You should have received a copy of the GNU Lesser General Public +| License along with this code; if not, write to the Free Software +| Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +| $Id$ +*/ + +#include "itdb_private.h" +#include <string.h> + +/* Generate a new Itdb_Track structure */ +Itdb_Track *itdb_track_new (void) +{ + Itdb_Track *track = g_new0 (Itdb_Track, 1); + + track->unk020 = 1; + return track; +} + + +/* Add @track to @itdb->tracks at position @pos (or at the end if pos + is -1). Application is responsible to also add it to the master + playlist. */ +void itdb_track_add (Itdb_iTunesDB *itdb, Itdb_Track *track, gint32 pos) +{ + g_return_if_fail (itdb); + g_return_if_fail (track); + g_return_if_fail (!track->userdata || track->userdata_duplicate); + + track->itdb = itdb; + + if (pos == -1) itdb->tracks = g_list_append (itdb->tracks, track); + else itdb->tracks = g_list_insert (itdb->tracks, track, pos); +} + +/* Free the memory taken by @track */ +void itdb_track_free (Itdb_Track *track) +{ + g_return_if_fail (track); + + g_free (track->album); + g_free (track->artist); + g_free (track->title); + g_free (track->genre); + g_free (track->comment); + g_free (track->composer); + g_free (track->fdesc); + g_free (track->grouping); + g_free (track->ipod_path); + if (track->userdata && track->userdata_destroy) + (*track->userdata_destroy) (track->userdata); + g_free (track); +} + +/* Remove track @track and free memory */ +void itdb_track_remove (Itdb_Track *track) +{ + Itdb_iTunesDB *itdb; + + g_return_if_fail (track); + itdb = track->itdb; + g_return_if_fail (itdb); + + itdb->tracks = g_list_remove (itdb->tracks, track); + itdb_track_free (track); +} + +/* Remove track @track but do not free memory */ +/* track->itdb is set to NULL */ +void itdb_track_unlink (Itdb_Track *track) +{ + Itdb_iTunesDB *itdb; + + g_return_if_fail (track); + itdb = track->itdb; + g_return_if_fail (itdb); + + itdb->tracks = g_list_remove (itdb->tracks, track); + track->itdb = NULL; +} + +/* Duplicate an existing track */ +Itdb_Track *itdb_track_duplicate (Itdb_Track *tr) +{ + Itdb_Track *tr_dup; + + g_return_val_if_fail (tr, NULL); + g_return_val_if_fail (!tr->userdata || tr->userdata_duplicate, NULL); + + tr_dup = g_new0 (Itdb_Track, 1); + memcpy (tr_dup, tr, sizeof (Itdb_Track)); + + /* clear itdb pointer */ + tr_dup->itdb = NULL; + + /* copy strings */ + tr_dup->album = g_strdup (tr->album); + tr_dup->artist = g_strdup (tr->artist); + tr_dup->title = g_strdup (tr->title); + tr_dup->genre = g_strdup (tr->genre); + tr_dup->comment = g_strdup (tr->comment); + tr_dup->composer = g_strdup (tr->composer); + tr_dup->fdesc = g_strdup (tr->fdesc); + tr_dup->grouping = g_strdup (tr->grouping); + tr_dup->ipod_path = g_strdup (tr->ipod_path); + + /* Copy userdata */ + if (tr->userdata) + tr_dup->userdata = tr->userdata_duplicate (tr->userdata); + + return tr_dup; +} + + + +/* Returns the track with the ID @id or NULL if the ID cannot be + * found. */ +/* Looking up tracks by ID is not really a good idea because the IDs + are created by itdb just before export. The functions are here + because they are needed during import of the iTunesDB which is + referencing tracks by IDs */ +/* This function is very slow -- if you need to lookup many IDs use + the functions itdb_track_id_tree_create(), + itdb_track_id_tree_destroy(), and itdb_track_id_tree_by_id() below. */ +Itdb_Track *itdb_track_by_id (Itdb_iTunesDB *itdb, guint32 id) +{ + GList *gl; + + g_return_val_if_fail (itdb, NULL); + + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *track = gl->data; + if (track->id == id) return track; + } + return NULL; +} + +static gint track_id_compare (gconstpointer a, gconstpointer b) +{ + if (*(guint32*) a == *(guint32*) b) + return 0; + if (*(guint32*) a > *(guint32*) b) + return 1; + return -1; +} + + +/* Creates a balanced-binary tree for quick ID lookup that is used in + itdb_track_by_id_tree() function below */ +GTree *itdb_track_id_tree_create (Itdb_iTunesDB *itdb) +{ + GTree *idtree; + GList *gl; + + g_return_val_if_fail (itdb, NULL); + + idtree = g_tree_new (track_id_compare); + + for (gl=itdb->tracks; gl; gl=gl->next) + { + Itdb_Track *tr = gl->data; + g_return_val_if_fail (tr, NULL); + g_tree_insert (idtree, &tr->id, tr); + } + return idtree; +} + +/* free memory of @idtree */ +void itdb_track_id_tree_destroy (GTree *idtree) +{ + g_return_if_fail (idtree); + + g_tree_destroy (idtree); +} + +/* lookup track by @id using @idtree for quicker reference */ +Itdb_Track *itdb_track_id_tree_by_id (GTree *idtree, guint32 id) +{ + g_return_val_if_fail (idtree, NULL); + + return (Itdb_Track *)g_tree_lookup (idtree, &id); +} diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 0000000..a436696 --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,7 @@ +noinst_PROGRAMS=test-itdb + +INCLUDES=$(LIBGPOD_CFLAGS) -I$(top_srcdir)/src -DPACKAGE_LOCALE_DIR=\""$(prefix)/$(DATADIRNAME)/locale"\" +LIBS=$(LIBGPOD_LIBS) + +test_itdb_SOURCES = itdb_main.c +test_itdb_LDADD = $(top_builddir)/src/libgpod.la diff --git a/tests/itdb_main.c b/tests/itdb_main.c new file mode 100644 index 0000000..fd81cff --- /dev/null +++ b/tests/itdb_main.c @@ -0,0 +1,88 @@ +/* +| Copyright (C) 2002-2003 Jorg Schuler <jcsjcs at users.sourceforge.net> +| Part of the gtkpod project. +| +| URL: http://gtkpod.sourceforge.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 +| +| iTunes and iPod are trademarks of Apple +| +| This product is not supported/written/published by Apple! +| +| $Id$ +*/ +/* + * Initial main.c file generated by Glade. Edit as required. + * Glade will not overwrite this file. + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include <libintl.h> + +#include "itdb.h" + +int +main (int argc, char *argv[]) +{ +#ifdef ENABLE_NLS + bindtextdomain (GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); +#endif + + GError *error=NULL; + Itdb_iTunesDB *itdb; + + if (argc == 2) + itdb = itdb_parse_file (argv[1], &error); + else itdb = itdb_parse_file ("/home/jcs/.gtkpod/iTunesDB20050102", + &error); + + printf ("%p\n", itdb); + if (error) + { + if (error->message) + puts (error->message); + g_error_free (error); + error = NULL; + } + + if (itdb) + { + printf ("tracks: %d\n", g_list_length (itdb->tracks)); + printf ("playlists: %d\n", g_list_length (itdb->playlists)); + + itdb_write_file (itdb, "/home/jcs/.gtkpod/iTunesDB", &error); + if (error) + { + if (error->message) + puts (error->message); + g_error_free (error); + error = NULL; + } + } + + itdb_free (itdb); + + /* all the cleanup is already done in gtkpod_main_quit() in misc.c */ + return 0; +} |