From 0ab16882c390a86d28e26475b4a5e103e1e1c928 Mon Sep 17 00:00:00 2001 From: Christophe Fergeau Date: Sat, 10 Sep 2005 08:34:34 +0000 Subject: Initial import git-svn-id: https://gtkpod.svn.sf.net/svnroot/gtkpod/libgpod/trunk@1080 f01d2545-417e-4e96-918e-98f8d0dbbcb6 --- AUTHORS | 2 + COPYING | 481 +++++++ ChangeLog | 19 + INSTALL | 229 ++++ Makefile.am | 16 + NEWS | 0 README | 0 autogen.sh | 20 + configure.ac | 120 ++ libgpod-1.0.pc.in | 11 + po/ChangeLog | 0 po/Makefile.in.in | 263 ++++ po/POTFILES.in | 2 + src/Makefile.am | 18 + src/itdb.h | 504 +++++++ src/itdb_itunesdb.c | 3622 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/itdb_playlist.c | 1290 ++++++++++++++++++ src/itdb_private.h | 100 ++ src/itdb_track.c | 205 +++ tests/Makefile.am | 7 + tests/itdb_main.c | 88 ++ 21 files changed, 6997 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100755 autogen.sh create mode 100644 configure.ac create mode 100644 libgpod-1.0.pc.in create mode 100644 po/ChangeLog create mode 100644 po/Makefile.in.in create mode 100644 po/POTFILES.in create mode 100644 src/Makefile.am create mode 100644 src/itdb.h create mode 100644 src/itdb_itunesdb.c create mode 100644 src/itdb_playlist.c create mode 100644 src/itdb_private.h create mode 100644 src/itdb_track.c create mode 100644 tests/Makefile.am create mode 100644 tests/itdb_main.c diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..d0a7de6 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Jorg Schuler (original gtkpod code) +Christophe Fergeau (libraryfication) diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..eb685a5 --- /dev/null +++ b/COPYING @@ -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. + + + Copyright (C) + + 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. + + , 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 + + * 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 diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..a4b3414 --- /dev/null +++ b/INSTALL @@ -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/NEWS b/NEWS new file mode 100644 index 0000000..e69de29 diff --git a/README b/README new file mode 100644 index 0000000..e69de29 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 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 +# +# 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 to use GETTEXT_PACKAGE +# instead of PACKAGE and to look for po2tbl in ./ not in intl/ +# +# - Modified by jacob berkman to install +# Makefile.in.in and po2tbl.sed.in for use with glib-gettextize +# +# - Modified by Rodney Dawes 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 +| 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 . +| +| 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 +#endif + +#include +#include + + +/* 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 +| 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 . +| +| 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 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include "itdb_private.h" +#include + +#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; icontents[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; ierror) { 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; iplaycounts = 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; ifield = 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 (ierror, + 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; ialbum = 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; iitdb->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; ierror) 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: , + containing the number of playlists, : first + playlist header. Here we just scan everything until we find + the first 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; ierror) 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 +| Part of the gtkpod project. +| +| Based on itunessd.c written by Steve Wahl for gtkpod-0.88: +| +| Copyright 2005 Steve Wahl +| +| 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 . +| +| 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" and copied + to @track->ipod_path. If this file already exists, + 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 +| 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 +#include + +/* 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 +| 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 +#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 +| 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 + +/* 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 +| 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 +#endif + +#include +#include +#include +#include + +#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; +} -- cgit