├── .gitignore ├── titl-core ├── src │ ├── test │ │ ├── resources │ │ │ ├── Empty iTunes 8.0 Library.itl │ │ │ ├── Empty iTunes 10.0 Library.itl │ │ │ ├── Empty iTunes 10.0.1 Library.itl │ │ │ ├── Empty iTunes 10.1 Library.itl │ │ │ ├── Empty iTunes 10.2 Library.itl │ │ │ ├── Empty iTunes 8.0.1 Library.itl │ │ │ ├── Empty iTunes 9.0.3 Library.itl │ │ │ ├── Empty iTunes 9.2.0 Library.itl │ │ │ ├── iTunes 8.0.1 Library TMBG.itl │ │ │ ├── Minimal iTunes 8.0.1 Library.itl │ │ │ ├── Empty OS X iTunes 11.1.5 Library.itl │ │ │ ├── iTunes 8.0.1 Library with TV show.itl │ │ │ ├── iTunes 10.2.2 Library with single track.itl │ │ │ └── iTunes 10.2.2 Library with single track with artwork.itl │ │ └── java │ │ │ └── org │ │ │ └── kafsemo │ │ │ └── titl │ │ │ ├── TestParseLibraryWithTvShows.java │ │ │ ├── TestInputImpl.java │ │ │ ├── TestProcessLibrary.java │ │ │ ├── TestITunesVersion.java │ │ │ ├── art │ │ │ └── TestArtworkFile.java │ │ │ ├── tools │ │ │ ├── TestMain.java │ │ │ └── TestMoveMusic.java │ │ │ ├── TestDates.java │ │ │ ├── TestBase64.java │ │ │ ├── TestUtil.java │ │ │ ├── TestHohm.java │ │ │ ├── TestHdfm.java │ │ │ ├── TestHohmPodcast.java │ │ │ └── TestParseLibrary.java │ └── main │ │ └── java │ │ └── org │ │ └── kafsemo │ │ └── titl │ │ ├── FlippedInputImpl.java │ │ ├── Input.java │ │ ├── ItlException.java │ │ ├── ITunesVersion.java │ │ ├── tools │ │ ├── ShufflePlaylist.java │ │ ├── Main.java │ │ ├── ExportRatings.java │ │ ├── ShowAllArtwork.java │ │ ├── MoveMusic.java │ │ └── ArtworkWebPage.java │ │ ├── diag │ │ └── InputRange.java │ │ ├── Artwork.java │ │ ├── Podcast.java │ │ ├── Playlist.java │ │ ├── Dates.java │ │ ├── PlaylistRawItems.java │ │ ├── art │ │ ├── ArtworkFile.java │ │ ├── ExtractArt.java │ │ └── AlbumArtworkDirectory.java │ │ ├── UnknownHohmException.java │ │ ├── Base64.java │ │ ├── InputImpl.java │ │ ├── Util.java │ │ ├── Library.java │ │ ├── HohmPodcast.java │ │ ├── ProcessLibrary.java │ │ ├── Track.java │ │ ├── Hdfm.java │ │ └── RandomizePlaylist.java └── pom.xml ├── README.md ├── BOILERPLATE ├── titl-rdf ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── kafsemo │ └── titl │ └── rdf │ └── LibraryAsRdf.java ├── pom.xml └── LGPL-3 /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .classpath 3 | .project 4 | .settings/ 5 | -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 8.0 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 8.0 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 10.0 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 10.0 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 10.0.1 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 10.0.1 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 10.1 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 10.1 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 10.2 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 10.2 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 8.0.1 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 8.0.1 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 9.0.3 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 9.0.3 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty iTunes 9.2.0 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty iTunes 9.2.0 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/iTunes 8.0.1 Library TMBG.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/iTunes 8.0.1 Library TMBG.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Minimal iTunes 8.0.1 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Minimal iTunes 8.0.1 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/Empty OS X iTunes 11.1.5 Library.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/Empty OS X iTunes 11.1.5 Library.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/iTunes 8.0.1 Library with TV show.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/iTunes 8.0.1 Library with TV show.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/iTunes 10.2.2 Library with single track.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/iTunes 10.2.2 Library with single track.itl -------------------------------------------------------------------------------- /titl-core/src/test/resources/iTunes 10.2.2 Library with single track with artwork.itl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephw/titl/HEAD/titl-core/src/test/resources/iTunes 10.2.2 Library with single track with artwork.itl -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##titl - Tools for iTunes Libraries 2 | 3 | A Java library for parsing iTunes' proprietary library file, 4 | along with command-line tools for modifying and extracting. 5 | 6 | See the 7 | [Tools](https://github.com/josephw/titl/wiki/Tools) wiki page for 8 | more details. 9 | 10 | ###License 11 | The GNU Lesser General Public License, v3 or later. 12 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/FlippedInputImpl.java: -------------------------------------------------------------------------------- 1 | package org.kafsemo.titl; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | public class FlippedInputImpl extends InputImpl 7 | { 8 | public FlippedInputImpl(InputStream in) 9 | { 10 | super(in); 11 | } 12 | 13 | @Override 14 | public int readInt() throws IOException 15 | { 16 | return Integer.reverseBytes(super.readInt()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestParseLibraryWithTvShows.java: -------------------------------------------------------------------------------- 1 | package org.kafsemo.titl; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | 9 | import org.junit.Test; 10 | 11 | public class TestParseLibraryWithTvShows 12 | { 13 | @Test 14 | public void testParseEmptyItunes80Library() throws IOException, ItlException 15 | { 16 | File f = new File("src/test/resources/iTunes 8.0.1 Library with TV show.itl"); 17 | 18 | Library lib = ParseLibrary.parse(f); 19 | assertNotNull(lib); 20 | assertEquals("8.0.2", lib.getVersion()); 21 | assertEquals("file://localhost/C:/Users/Joseph/Music/iTunes/iTunes%20Music/", lib.getMusicFolder()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BOILERPLATE: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | -------------------------------------------------------------------------------- /titl-rdf/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.kafsemo 7 | titl 8 | 0.3-SNAPSHOT 9 | 10 | 11 | titl-rdf 12 | 13 | 14 | 15 | org.kafsemo 16 | titl-core 17 | ${project.version} 18 | 19 | 20 | org.openrdf.sesame 21 | sesame-rio-turtle 22 | 2.7.10 23 | 24 | 25 | junit 26 | junit 27 | test 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestInputImpl.java: -------------------------------------------------------------------------------- 1 | package org.kafsemo.titl; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.IOException; 7 | 8 | import org.junit.Test; 9 | 10 | public class TestInputImpl 11 | { 12 | @Test 13 | public void getPositionReflectsReading() throws IOException 14 | { 15 | Input in = new InputImpl(new ByteArrayInputStream(new byte[1024])); 16 | 17 | assertEquals(0, in.getPosition()); 18 | in.readUnsignedByte(); 19 | assertEquals(1, in.getPosition()); 20 | in.readShort(); 21 | assertEquals(3, in.getPosition()); 22 | in.readInt(); 23 | assertEquals(7, in.getPosition()); 24 | in.readFully(new byte[1]); 25 | assertEquals(8, in.getPosition()); 26 | in.skipBytes(1); 27 | assertEquals(9, in.getPosition()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /titl-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.kafsemo 7 | titl 8 | 0.3-SNAPSHOT 9 | 10 | 11 | titl-core 12 | 13 | 14 | 15 | junit 16 | junit 17 | test 18 | 19 | 20 | 21 | 22 | 23 | 24 | maven-jar-plugin 25 | 2.4 26 | 27 | 28 | false 29 | 30 | org.kafsemo.titl.tools.Main 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Input.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.IOException; 22 | 23 | public interface Input 24 | { 25 | int readUnsignedByte() throws IOException; 26 | short readShort() throws IOException; 27 | int readInt() throws IOException; 28 | 29 | void readFully(byte b[]) throws IOException; 30 | 31 | int skipBytes(int n) throws IOException; 32 | long getPosition(); 33 | } 34 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/ItlException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | 22 | /** 23 | * An exception with the structure of a library file. 24 | * 25 | * @author Joseph 26 | */ 27 | public class ItlException extends Exception 28 | { 29 | public ItlException(String message, Throwable cause) 30 | { 31 | super(message, cause); 32 | } 33 | 34 | public ItlException(String message) 35 | { 36 | super(message); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/ITunesVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | public class ITunesVersion 22 | { 23 | public static boolean isAtLeast(String fullVersion, int majorVersion) 24 | { 25 | int endOfFirstNumber = fullVersion.indexOf('.'); 26 | if (endOfFirstNumber < 0) { 27 | endOfFirstNumber = fullVersion.length(); 28 | } 29 | 30 | try { 31 | return Integer.parseInt(fullVersion.substring(0, endOfFirstNumber)) >= majorVersion; 32 | } catch (NumberFormatException nfe) { 33 | return false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.kafsemo 5 | titl 6 | pom 7 | 8 | 0.3-SNAPSHOT 9 | 10 | 11 | titl-core 12 | titl-rdf 13 | 14 | 15 | 16 | 17 | 18 | junit 19 | junit 20 | 4.11 21 | test 22 | 23 | 24 | 25 | 26 | 27 | UTF-8 28 | 1.6 29 | 1.6 30 | 31 | 32 | 33 | 34 | LGPLv3 35 | http://www.gnu.org/licenses/lgpl-3.0.txt 36 | repo 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | maven-compiler-plugin 45 | 3.1 46 | 47 | 1.6 48 | 1.6 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestProcessLibrary.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | 23 | import org.junit.Test; 24 | import org.kafsemo.titl.ProcessLibrary; 25 | 26 | public class TestProcessLibrary 27 | { 28 | @Test 29 | public void chooseCorrectEncoding() 30 | { 31 | ProcessLibrary.Encoding enc; 32 | 33 | enc = ProcessLibrary.chooseEncoding(""); 34 | assertEquals(ProcessLibrary.Encoding.ISO88591, enc); 35 | 36 | enc = ProcessLibrary.chooseEncoding("Test"); 37 | assertEquals(ProcessLibrary.Encoding.ISO88591, enc); 38 | 39 | enc = ProcessLibrary.chooseEncoding("T€st"); 40 | assertEquals(ProcessLibrary.Encoding.UTF16BE, enc); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestITunesVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertFalse; 22 | import static org.junit.Assert.assertTrue; 23 | 24 | import org.junit.Test; 25 | 26 | public class TestITunesVersion 27 | { 28 | @Test 29 | public void versionsAreAtLeastTen() 30 | { 31 | assertTrue(ITunesVersion.isAtLeast("10", 10)); 32 | assertTrue(ITunesVersion.isAtLeast("10.1", 10)); 33 | assertTrue(ITunesVersion.isAtLeast("11", 10)); 34 | } 35 | 36 | @Test 37 | public void earlierVersionsAreNotAtLeastTen() 38 | { 39 | assertFalse(ITunesVersion.isAtLeast("9", 10)); 40 | assertFalse(ITunesVersion.isAtLeast("8.0", 10)); 41 | assertFalse(ITunesVersion.isAtLeast("8.0.1", 10)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/tools/ShufflePlaylist.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2013 Lael Jones 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | 24 | import org.kafsemo.titl.ItlException; 25 | import org.kafsemo.titl.RandomizePlaylist; 26 | 27 | 28 | /** 29 | * A simple command-line tool to shuffle the items in a playlist. 30 | * 31 | * @author Lael Jones 32 | */ 33 | public class ShufflePlaylist 34 | { 35 | public static void main(String[] args) throws IOException, ItlException 36 | { 37 | if (args.length != 3) { 38 | System.err.println("Usage: Shuffle Playlist "); 39 | System.exit(5); 40 | } 41 | 42 | RandomizePlaylist.randomizePlaylist(new File(args[0]), new File(args[1]), args[2]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/diag/InputRange.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.diag; 20 | 21 | public class InputRange 22 | { 23 | public final long origin; 24 | public int length; 25 | public String type; 26 | public Object more; 27 | public String details; 28 | 29 | public InputRange(long origin) 30 | { 31 | this.origin = origin; 32 | } 33 | 34 | public String toString() 35 | { 36 | StringBuilder sb = new StringBuilder(); 37 | sb.append(String.format("%08d: %s (%d)", origin, type, length)); 38 | if (more != null) { 39 | sb.append(" "); 40 | sb.append(more); 41 | } 42 | if (details != null) { 43 | sb.append(" \""); 44 | sb.append(details); 45 | sb.append("\""); 46 | } 47 | return sb.toString(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Artwork.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.util.Arrays; 22 | 23 | public class Artwork 24 | { 25 | private final byte[] persistentId; 26 | 27 | public String title, artist, appTitle; 28 | 29 | public Artwork() 30 | { 31 | persistentId = null; 32 | } 33 | 34 | public Artwork(byte[] id) 35 | { 36 | persistentId = Arrays.copyOf(id, id.length); 37 | } 38 | 39 | public void setTitle(String title) 40 | { 41 | this.title = title; 42 | } 43 | 44 | public void setArtist(String artist) 45 | { 46 | this.artist = artist; 47 | } 48 | 49 | public void setAppTitle(String appTitle) 50 | { 51 | this.appTitle = appTitle; 52 | } 53 | 54 | public byte[] getPersistentId() 55 | { 56 | return persistentId; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/tools/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.lang.reflect.Method; 23 | import java.util.Arrays; 24 | 25 | public class Main 26 | { 27 | public static void main(String... args) 28 | throws ClassNotFoundException, SecurityException, 29 | NoSuchMethodException, IllegalArgumentException, 30 | IllegalAccessException, InvocationTargetException 31 | { 32 | if (args.length < 1) 33 | { 34 | throw new IllegalArgumentException("Usage: Main "); 35 | } 36 | 37 | Class cls = Class.forName(Main.class.getPackage().getName() + "." + args[0]); 38 | 39 | Method mm = cls.getMethod("main", String[].class); 40 | 41 | String[] subArgs = Arrays.copyOfRange(args, 1, args.length); 42 | mm.invoke(null, (Object) subArgs); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/tools/ExportRatings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | 24 | import org.kafsemo.titl.ItlException; 25 | import org.kafsemo.titl.Library; 26 | import org.kafsemo.titl.ParseLibrary; 27 | import org.kafsemo.titl.Track; 28 | 29 | 30 | /** 31 | * A simple command-line tool to export ratings as a comma-separated list. 32 | * 33 | * @author Joseph 34 | */ 35 | public class ExportRatings 36 | { 37 | public static void main(String[] args) throws IOException, ItlException 38 | { 39 | if (args.length != 1) { 40 | System.err.println("Usage: ExportRatings "); 41 | System.exit(5); 42 | } 43 | 44 | Library lib = ParseLibrary.parse(new File(args[0])); 45 | 46 | for (Track t : lib.getTracks()) { 47 | if(t.getLocation() != null) { 48 | System.out.println(t.getLocation() + "," + t.getRating()); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Podcast.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.Collections; 24 | 25 | public class Podcast extends Track 26 | { 27 | private String podcastUrl; 28 | private String podcastTitle; 29 | private Collection podcastAuthors = new ArrayList(); 30 | 31 | public String getPodcastLocation() 32 | { 33 | return podcastUrl; 34 | } 35 | 36 | public void setPodcastLocation(String l) 37 | { 38 | this.podcastUrl = l; 39 | } 40 | 41 | public String getPodcastTitle() 42 | { 43 | return podcastTitle; 44 | } 45 | 46 | public void setPodcastTitle(String t) 47 | { 48 | this.podcastTitle = t; 49 | } 50 | 51 | public Collection getPodcastAuthors() 52 | { 53 | return Collections.unmodifiableCollection(podcastAuthors); 54 | } 55 | 56 | public void addPodcastAuthor(String author) 57 | { 58 | this.podcastAuthors.add(author); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Playlist.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | 25 | class Playlist 26 | { 27 | byte[] ppid; 28 | String title; 29 | // int id; 30 | public byte[] smartInfo; 31 | public byte[] smartCriteria; 32 | 33 | private final List items = new ArrayList(); 34 | private HohmPodcast hohmPodcast;; 35 | 36 | public byte[] getPpid() 37 | { 38 | return ppid; 39 | } 40 | 41 | public String getTitle() 42 | { 43 | return title; 44 | } 45 | 46 | public List getItems() 47 | { 48 | return items; 49 | } 50 | 51 | public void addItem(int key) 52 | { 53 | items.add(Integer.valueOf(key)); 54 | } 55 | 56 | public void setHohmPodcast(HohmPodcast parse) 57 | { 58 | this.hohmPodcast = parse; 59 | } 60 | 61 | public HohmPodcast getHohmPodcast() 62 | { 63 | return hohmPodcast; 64 | } 65 | 66 | // public int getId() 67 | // { 68 | // return -1; 69 | // } 70 | } 71 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Dates.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | import java.text.DateFormat; 21 | import java.text.SimpleDateFormat; 22 | import java.util.Calendar; 23 | import java.util.Date; 24 | import java.util.TimeZone; 25 | 26 | 27 | public class Dates 28 | { 29 | static String toString(Date d) 30 | { 31 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 32 | df.setTimeZone(TimeZone.getTimeZone("UTC")); 33 | 34 | return df.format(d); 35 | } 36 | 37 | public static Date fromMac(long seconds) 38 | { 39 | if (seconds == 0) 40 | { 41 | return null; 42 | } 43 | 44 | seconds &= 0xFFFFFFFFL; 45 | 46 | Calendar cal = Calendar.getInstance(); 47 | cal.setTimeZone(TimeZone.getTimeZone("Europe/London")); 48 | cal.clear(); 49 | cal.set(Calendar.YEAR, 1904); 50 | cal.set(Calendar.MONTH, 0); 51 | cal.set(Calendar.DATE, 1); 52 | 53 | long epoch = cal.getTimeInMillis() - 60 * 60 * 1000L; 54 | 55 | return new Date((seconds * 1000L) + epoch); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/art/TestArtworkFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.art; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | 23 | import org.junit.Test; 24 | 25 | public class TestArtworkFile 26 | { 27 | @Test 28 | public void fileFromDefinition() 29 | { 30 | ArtworkFile af = new ArtworkFile( 31 | ArtworkFile.Directory.Cache, 32 | new byte[8], 33 | 2); 34 | 35 | String f = af.toString(new byte[8]); 36 | 37 | assertEquals( 38 | "Cache/0000000000000000/00/00/00/0000000000000000-0000000000000000.itc2", 39 | f); 40 | } 41 | 42 | @Test 43 | public void fileFromDefinitionWithNumbers() 44 | { 45 | ArtworkFile af = new ArtworkFile( 46 | ArtworkFile.Directory.Cache, 47 | new byte[]{0, 1, 2, 3, 4, 5, 6, 7}, 48 | 1); 49 | 50 | String f = af.toString( 51 | new byte[]{8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}); 52 | 53 | assertEquals( 54 | "Cache/08090A0B0C0D0E0F/07/00/06/08090A0B0C0D0E0F-0001020304050607.itc", 55 | f); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/PlaylistRawItems.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2013 Lael Jones 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | /** 26 | * 27 | * @author Lael 28 | */ 29 | class PlaylistRawItems 30 | { 31 | public final int expectedItemCount; 32 | public long itemStartOffset; 33 | private final List itemIds = new ArrayList(); 34 | private final List itemBytes = new ArrayList(); 35 | 36 | /** 37 | * 38 | * @param expectedItemCount 39 | */ 40 | public PlaylistRawItems(int expectedItemCount) 41 | { 42 | this.expectedItemCount = expectedItemCount; 43 | } 44 | 45 | public List getItemIds() 46 | { 47 | return itemIds; 48 | } 49 | 50 | public List getRawItems() 51 | { 52 | return itemBytes; 53 | } 54 | 55 | public void addItem(int key, ByteArrayOutputStream bo) 56 | { 57 | itemIds.add(Integer.valueOf(key)); 58 | itemBytes.add(bo); 59 | } 60 | 61 | public int getExpectedItemCount() 62 | { 63 | return expectedItemCount; 64 | } 65 | 66 | public long getItemStartOffset() 67 | { 68 | return itemStartOffset; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/tools/TestMain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import static org.junit.Assert.fail; 22 | 23 | import java.io.IOException; 24 | import java.lang.reflect.InvocationTargetException; 25 | import java.util.Arrays; 26 | 27 | import org.junit.Test; 28 | 29 | public class TestMain 30 | { 31 | @Test(expected = IllegalArgumentException.class) 32 | public void exceptionWhenNoClassIsProvided() throws Exception 33 | { 34 | Main.main(); 35 | } 36 | 37 | @Test(expected = ClassNotFoundException.class) 38 | public void unknownClassesAreNotFound() throws Exception 39 | { 40 | Main.main("UnknownClass"); 41 | } 42 | 43 | @Test 44 | public void knownClassIsFound() throws Exception 45 | { 46 | Main.main("TestMain"); 47 | } 48 | 49 | @Test(expected = IOException.class) 50 | public void correctArgumentsArePassedToMainMethod() throws Throwable 51 | { 52 | try { 53 | Main.main("TestMain", "arg1", "arg2"); 54 | fail(); 55 | } catch (InvocationTargetException ite) { 56 | throw ite.getCause(); 57 | } 58 | } 59 | 60 | public static void main(String[] args) throws IOException 61 | { 62 | String[] expected = {"arg1", "arg2"}; 63 | 64 | if (Arrays.equals(expected, args)) { 65 | throw new IOException("Expected args received"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/art/ArtworkFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.art; 20 | 21 | import java.io.File; 22 | 23 | import org.kafsemo.titl.Util; 24 | 25 | public class ArtworkFile 26 | { 27 | enum Directory { 28 | Cache, 29 | Download 30 | } 31 | 32 | final Directory dir; 33 | final byte[] id; 34 | final int version; 35 | 36 | public ArtworkFile(Directory dir, byte[] id, int version) 37 | { 38 | this.dir = dir; 39 | this.id = id; 40 | this.version = version; 41 | } 42 | 43 | public File getFile(File artworkDirectory, byte[] libraryPersistentId) 44 | { 45 | return new File(artworkDirectory, toString(libraryPersistentId)); 46 | } 47 | 48 | public String toString(byte[] libraryPersistentId) 49 | { 50 | StringBuilder sb = new StringBuilder(); 51 | 52 | sb.append(dir.name()); 53 | sb.append('/'); 54 | 55 | String libDir = Util.pidToString(libraryPersistentId); 56 | 57 | sb.append(libDir); 58 | sb.append('/'); 59 | sb.append(String.format("%02d", id[7] & 0x0F)); 60 | sb.append('/'); 61 | sb.append(String.format("%02d", (id[7] >> 4) & 0x0F)); 62 | sb.append('/'); 63 | sb.append(String.format("%02d", id[6] & 0x0F)); 64 | 65 | sb.append('/' + libDir + "-" + Util.pidToString(id)); 66 | 67 | sb.append(".itc"); 68 | 69 | if (version != 1) { 70 | sb.append(version); 71 | } 72 | 73 | return sb.toString(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestDates.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.util.Date; 22 | 23 | import org.junit.Test; 24 | import org.kafsemo.titl.Dates; 25 | 26 | import junit.framework.TestCase; 27 | 28 | public class TestDates extends TestCase 29 | { 30 | @Test 31 | public void testToUnsigned() 32 | { 33 | int a = -992945054; 34 | 35 | long res = a; 36 | 37 | res = 0x100000000L + a; 38 | 39 | res = (long) a & 0xFFFFFFFFL; 40 | 41 | assertEquals(3302022242L, res); 42 | } 43 | 44 | @Test 45 | public void testToDate() 46 | { 47 | long a = 3302022242L; 48 | 49 | Date d = Dates.fromMac(a); 50 | 51 | assertEquals("2008-08-19T19:24:02Z", 52 | Dates.toString(d)); 53 | } 54 | 55 | @Test 56 | public void testToDate2() 57 | { 58 | long a = 3277369669L; 59 | 60 | Date d = Dates.fromMac(a); 61 | 62 | assertEquals("2007-11-08T11:27:49Z", 63 | Dates.toString(d)); 64 | } 65 | 66 | @Test 67 | public void testToDate3() 68 | { 69 | long a = 3300033502L; 70 | 71 | Date d = Dates.fromMac(a); 72 | 73 | assertEquals("2008-07-27T18:58:22Z", 74 | Dates.toString(d)); 75 | } 76 | 77 | @Test 78 | public void testFromLibraryFile() 79 | { 80 | int a = -988797720; 81 | 82 | Date d = Dates.fromMac(a); 83 | 84 | assertEquals("2008-10-06T19:26:16Z", 85 | Dates.toString(d)); 86 | } 87 | 88 | @Test 89 | public void zeroBecomesNullDate() 90 | { 91 | int a = 0; 92 | 93 | Date d = Dates.fromMac(a); 94 | assertNull(d); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/UnknownHohmException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | /** 22 | * An unknown hohm type was encountered. This exception includes 23 | * the contents to give a clue to the data. 24 | */ 25 | public class UnknownHohmException extends ItlException 26 | { 27 | private final int hohmType; 28 | private final byte[] contents; 29 | 30 | public UnknownHohmException(int hohmType, byte[] contents) 31 | { 32 | super(null); 33 | this.hohmType = hohmType; 34 | this.contents = contents; 35 | } 36 | 37 | @Override 38 | public String getMessage() 39 | { 40 | StringBuilder sb = new StringBuilder(); 41 | sb.append("Unknown type: "); 42 | sb.append(hohmType); 43 | sb.append(String.format(" (0x%X)", hohmType)); 44 | 45 | int len = Math.min(contents.length, 64); 46 | 47 | /* Hex */ 48 | sb.append('\n'); 49 | sb.append("Hex:"); 50 | for(int i = 0; i < len; i++) { 51 | sb.append(' '); 52 | sb.append(String.format("0x%02X", contents[i] & 0xFF)); 53 | } 54 | 55 | if (len < contents.length) { 56 | sb.append(" ..."); 57 | } 58 | 59 | len = Math.min(contents.length, 96); 60 | 61 | /* ASCII */ 62 | sb.append('\n'); 63 | sb.append("ASCII: "); 64 | for (int i = 0; i < len; i++) { 65 | /* Only show printable ASCII characters */ 66 | int c = contents[i] & 0xFF; 67 | if ((c >= 0x20) && (c <= 0x7E)) { 68 | sb.append((char) c); 69 | } else { 70 | sb.append('.'); 71 | } 72 | } 73 | 74 | if (len < contents.length) { 75 | sb.append(" ..."); 76 | } 77 | 78 | return sb.toString(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestBase64.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | 23 | import java.io.UnsupportedEncodingException; 24 | 25 | import org.junit.Test; 26 | import org.kafsemo.titl.Base64; 27 | 28 | public class TestBase64 29 | { 30 | @Test 31 | public void testDecodeEmpty() 32 | { 33 | byte[] ba = Base64.decode(""); 34 | assertEquals(0, ba.length); 35 | } 36 | 37 | @Test 38 | public void testDecode() throws UnsupportedEncodingException 39 | { 40 | byte[] ba = Base64.decode("VGVzdA=="); 41 | assertEquals("Test", new String(ba, "US-ASCII")); 42 | } 43 | 44 | @Test 45 | public void testDecodeWhitespaceStripped() throws UnsupportedEncodingException 46 | { 47 | byte[] ba = Base64.decode("VGV\nzdA=="); 48 | assertEquals("Test", new String(ba, "US-ASCII")); 49 | } 50 | 51 | @Test 52 | public void testDecodeNoPadding() throws UnsupportedEncodingException 53 | { 54 | byte[] ba = Base64.decode("QUJD"); 55 | assertEquals("ABC", new String(ba, "US-ASCII")); 56 | } 57 | 58 | @Test 59 | public void testDecodeOnePaddingChar() throws UnsupportedEncodingException 60 | { 61 | byte[] ba = Base64.decode("ISE="); 62 | assertEquals("!!", new String(ba, "US-ASCII")); 63 | } 64 | 65 | /** 66 | * Strings must only contain the defined Base64 characters.0 67 | */ 68 | @Test(expected = IllegalArgumentException.class) 69 | public void testDecodeBadCharacter() 70 | { 71 | Base64.decode("!!!!"); 72 | } 73 | 74 | /** 75 | * Strings must always be a multiple of four characters long. 76 | */ 77 | @Test(expected = IllegalArgumentException.class) 78 | public void testDecodeBadStringLength() 79 | { 80 | Base64.decode("ISE"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Base64.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | 23 | public class Base64 24 | { 25 | public static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 26 | public static final char PAD = '='; 27 | 28 | private static final int val(char c) 29 | { 30 | int idx = CHARS.indexOf(c); 31 | if (idx < 0) { 32 | throw new IllegalArgumentException("Bad Base64 character: " + c); 33 | } 34 | return idx; 35 | } 36 | 37 | public static byte[] decode(String s) 38 | { 39 | s = s.replaceAll("\\s+", ""); 40 | 41 | char[] ca = s.toCharArray(); 42 | 43 | if (ca.length % 4 != 0) 44 | { 45 | throw new IllegalArgumentException(); 46 | } 47 | 48 | ByteArrayOutputStream baos = new ByteArrayOutputStream((ca.length * 3 + 3) / 4); 49 | 50 | for (int i = 0; i < ca.length; i += 4) { 51 | if (ca[i + 2] == PAD) { 52 | int v1 = val(ca[i]), 53 | v2 = val(ca[i + 1]); 54 | 55 | byte b1 = (byte) ((v1 << 2) | ((v2 >> 4) & 0x03)); 56 | 57 | baos.write(b1); 58 | } else if (ca[i + 3] == PAD) { 59 | int v1 = val(ca[i]), 60 | v2 = val(ca[i + 1]), 61 | v3 = val(ca[i + 2]); 62 | 63 | byte b1 = (byte) ((v1 << 2) | ((v2 >> 4) & 0x03)); 64 | byte b2 = (byte) (((v2 << 4) & 0xF0) | ((v3 >> 2) & 0x0F)); 65 | 66 | baos.write(b1); 67 | baos.write(b2); 68 | } else { 69 | int v1 = val(ca[i]), 70 | v2 = val(ca[i + 1]), 71 | v3 = val(ca[i + 2]), 72 | v4 = val(ca[i + 3]); 73 | 74 | byte b1 = (byte) ((v1 << 2) | ((v2 >> 4) & 0x03)); 75 | byte b2 = (byte) (((v2 << 4) & 0xF0) | ((v3 >> 2) & 0x0F)); 76 | byte b3 = (byte) (((v3 << 6) & 0x40) | v4); 77 | 78 | baos.write(b1); 79 | baos.write(b2); 80 | baos.write(b3); 81 | } 82 | } 83 | 84 | return baos.toByteArray(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertFalse; 23 | import static org.junit.Assert.assertTrue; 24 | 25 | import java.io.UnsupportedEncodingException; 26 | 27 | import org.junit.Test; 28 | import org.kafsemo.titl.Util; 29 | 30 | public class TestUtil 31 | { 32 | @Test 33 | public void testToString() throws UnsupportedEncodingException 34 | { 35 | int hdfmInt = 1751410285; 36 | assertEquals("hdfm", Util.toString(hdfmInt)); 37 | } 38 | 39 | @Test 40 | public void testFromString() throws UnsupportedEncodingException 41 | { 42 | String s = "hdfm"; 43 | 44 | assertEquals(1751410285, Util.fromString(s)); 45 | } 46 | 47 | @Test 48 | public void isPlausibleIdentifier() throws UnsupportedEncodingException 49 | { 50 | assertTrue("Letters form a valid identifier", Util.isIdentifier("test")); 51 | assertTrue("Letters form a valid identifier", Util.isIdentifier("0123")); 52 | assertFalse("Non-alphanumeric characters are not a valid identifer", 53 | Util.isIdentifier("xĽ")); 54 | 55 | assertFalse("The empty string is not a valid identifier", Util.isIdentifier("")); 56 | } 57 | 58 | @Test 59 | public void testPidToString() 60 | { 61 | byte[] libraryPersistentId = { 62 | 0x30, 0x14, (byte) 0x96, (byte) 0xC1, 0x63, (byte) 0xC0, (byte) 0xDF, 0x40 63 | }; 64 | 65 | assertEquals("301496C163C0DF40", Util.pidToString(libraryPersistentId)); 66 | } 67 | 68 | @Test(expected = IllegalArgumentException.class) 69 | public void failsWithBadIdStringLength() 70 | { 71 | Util.fromString(""); 72 | } 73 | 74 | @Test(expected = IllegalArgumentException.class) 75 | public void failsWithBadIdStringCharacters() 76 | { 77 | Util.fromString("€€€€"); 78 | } 79 | 80 | @Test 81 | public void pathToUrl() 82 | { 83 | String p = "C:\\Users\\Joseph\\Music\\iTunes\\iTunes Music\\Podcasts\\CASH Music_ Kristin Hersh\\Fortune.mp3"; 84 | 85 | String url = Util.toUrl(p); 86 | 87 | assertEquals("A Windows file path should be converted to a URL", 88 | "file://localhost/C:/Users/Joseph/Music/iTunes/iTunes%20Music/Podcasts/CASH%20Music_%20Kristin%20Hersh/Fortune.mp3", 89 | url); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/InputImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.DataInput; 22 | import java.io.DataInputStream; 23 | import java.io.FilterInputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | 27 | public class InputImpl implements Input 28 | { 29 | private final InputStream in; 30 | private final DataInput di; 31 | private long position = 0; 32 | 33 | public InputImpl(InputStream in) 34 | { 35 | this.in = new CountingInputStream(in); 36 | this.di = new DataInputStream(this.in); 37 | } 38 | 39 | public long getPosition() 40 | { 41 | return position; 42 | } 43 | 44 | private class CountingInputStream extends FilterInputStream 45 | { 46 | public CountingInputStream(InputStream in) 47 | { 48 | super(in); 49 | } 50 | 51 | @Override 52 | public int read() throws IOException 53 | { 54 | int r = super.read(); 55 | if (r >= 0) { 56 | position++; 57 | } 58 | return r; 59 | } 60 | 61 | @Override 62 | public int read(byte[] b, int off, int len) throws IOException 63 | { 64 | int count = super.read(b, off, len); 65 | if (count >= 0) { 66 | position += count; 67 | } 68 | return count; 69 | } 70 | 71 | @Override 72 | public long skip(long n) throws IOException 73 | { 74 | long count = super.skip(n); 75 | if (count >= 0) { 76 | position += count; 77 | } 78 | return count; 79 | } 80 | } 81 | 82 | @Override 83 | public int readUnsignedByte() throws IOException 84 | { 85 | return di.readUnsignedByte(); 86 | } 87 | 88 | @Override 89 | public short readShort() throws IOException 90 | { 91 | return di.readShort(); 92 | } 93 | 94 | @Override 95 | public int readInt() throws IOException 96 | { 97 | return di.readInt(); 98 | } 99 | 100 | @Override 101 | public void readFully(byte[] b) throws IOException 102 | { 103 | di.readFully(b); 104 | } 105 | 106 | @Override 107 | public int skipBytes(int n) throws IOException 108 | { 109 | return di.skipBytes(n); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | import java.io.IOException; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | import java.util.regex.Pattern; 24 | 25 | 26 | public class Util 27 | { 28 | public static void assertEquals(String expected, String actual) 29 | throws IOException 30 | { 31 | if(!expected.equals(actual)) 32 | { 33 | throw new IOException("Expected: " + expected + " but was: " + actual); 34 | } 35 | } 36 | 37 | public static String toString(int tag) 38 | { 39 | char[] ba = new char[4]; 40 | for (int i = 0; i < 4; i++) { 41 | ba[i] = (char) (tag >> ((3 - i) * 8) & 0xff); 42 | } 43 | 44 | return new String(ba); 45 | } 46 | 47 | public static int fromString(String s) throws IllegalArgumentException 48 | { 49 | char[] ca = s.toCharArray(); 50 | if (ca.length != 4) { 51 | throw new IllegalArgumentException(); 52 | } 53 | 54 | int res = 0; 55 | for (int i = 0; i < 4; i++) { 56 | int c = ca[i]; 57 | if (c > 0xff) { 58 | throw new IllegalArgumentException(); 59 | } 60 | 61 | res |= (c << ((3 - i) * 8)); 62 | } 63 | 64 | return res; 65 | } 66 | 67 | public static String pidToString(byte[] libraryPersistentId) 68 | { 69 | StringBuffer sb = new StringBuffer(libraryPersistentId.length * 2); 70 | for (byte b : libraryPersistentId) { 71 | sb.append(String.format("%02X", b & 0xFF)); 72 | } 73 | 74 | return sb.toString(); 75 | } 76 | 77 | /** 78 | * Converts a Windows path into a file: URL, as used by iTunes in its XML. 79 | * 80 | * @param p 81 | * @return 82 | */ 83 | public static String toUrl(String p) 84 | { 85 | try { 86 | URI uri; 87 | uri = new URI("file", "localhost", '/' + p.replace('\\', '/'), null); 88 | return uri.toASCIIString(); 89 | } catch (URISyntaxException use) { 90 | throw new RuntimeException(use); 91 | } 92 | } 93 | 94 | private static Pattern ID_PATTERN = Pattern.compile("[a-zA-Z0-9]+"); 95 | 96 | public static boolean isIdentifier(String id) 97 | { 98 | return ID_PATTERN.matcher(id).matches(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/tools/TestMoveMusic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertFalse; 23 | import static org.junit.Assert.assertNull; 24 | import static org.junit.Assert.assertTrue; 25 | 26 | import org.junit.Test; 27 | 28 | public class TestMoveMusic 29 | { 30 | @Test 31 | public void enoughArguments() 32 | { 33 | assertNull("At least three arguments must be provided", MoveMusic.fromArgs(new String[0])); 34 | assertNull("At least three arguments must be provided", MoveMusic.fromArgs(new String[1])); 35 | assertNull("At least three arguments must be provided", MoveMusic.fromArgs(new String[2])); 36 | } 37 | 38 | @Test 39 | public void minimalArguments() 40 | { 41 | MoveMusic mm; 42 | 43 | mm = MoveMusic.fromArgs(new String[]{"lib", "a", "b"}); 44 | assertEquals("lib", mm.getLibraryFilename()); 45 | assertEquals("a", mm.getOrigDir()); 46 | assertEquals("b", mm.getDestDir()); 47 | assertFalse("Default is not to use URLs", mm.isUseUrls()); 48 | } 49 | 50 | @Test 51 | public void useUrlsFlag() 52 | { 53 | MoveMusic mm; 54 | 55 | mm = MoveMusic.fromArgs(new String[]{"--use-urls", "lib", "a", "b"}); 56 | assertEquals("lib", mm.getLibraryFilename()); 57 | assertEquals("a", mm.getOrigDir()); 58 | assertEquals("b", mm.getDestDir()); 59 | assertTrue("Use URLs when requested", mm.isUseUrls()); 60 | } 61 | 62 | @Test 63 | public void badArguments() 64 | { 65 | assertNull("A switch is not an acceptable argument", 66 | MoveMusic.fromArgs(new String[]{"--use-urls", "a"})); 67 | 68 | assertNull("Too many arguments is a problem", 69 | MoveMusic.fromArgs(new String[]{"a", "b", "c","d"})); 70 | 71 | assertNull("Too many arguments is a problem", 72 | MoveMusic.fromArgs(new String[]{"--use-urls", "a", "b", "c", "d"})); 73 | 74 | assertNull("An unknown switch is a problem", 75 | MoveMusic.fromArgs(new String[]{"--unknown", "a", "b"})); 76 | } 77 | 78 | @Test 79 | public void testConversion() 80 | { 81 | MoveMusic mm = new MoveMusic(null, "before-", "after-"); 82 | assertEquals("xxx", mm.convert("xxx")); 83 | assertEquals("after-xx", mm.convert("before-xx")); 84 | assertEquals("after-xx", mm.convert("after-xx")); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestHohm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | 23 | import java.io.ByteArrayInputStream; 24 | import java.io.IOException; 25 | 26 | import org.junit.Test; 27 | 28 | public class TestHohm 29 | { 30 | private static byte[] sampleUtf16Chunk = { 31 | 0x68, 0x6f, 0x68, 0x6d, 32 | 0x00, 0x00, 0x00, 0x18, 33 | 0x00, 0x00, 0x00, 0x3c, 34 | 0x00, 0x00, 0x00, 0x64, 35 | 0x00, 0x00, 0x00, 0x03, 36 | 0x00, 0x00, 0x00, 0x00, 37 | 0x00, 0x00, 0x00, 0x01, 38 | 0x00, 0x00, 0x00, 0x14, 39 | 0x00, 0x00, 0x00, 0x00, 40 | 0x00, 0x00, 0x00, 0x00, 41 | 0x00, 0x39, 0x00, 0x30, 42 | 0x20, 0x19, 0x00, 0x73, 43 | 0x00, 0x20, 0x00, 0x4d, 44 | 0x00, 0x75, 0x00, 0x73, 45 | 0x00, 0x69, 0x00, 0x63 46 | }; 47 | 48 | private static byte[] sampleLatin1Chunk = { 49 | 0x68, 0x6f, 0x68, 0x6d, 50 | 0x00, 0x00, 0x00, 0x18, 51 | 0x00, 0x00, 0x00, 0x34, 52 | 0x00, 0x00, 0x00, 0x64, 53 | 0x00, 0x00, 0x00, 0x03, 54 | 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x03, 56 | 0x00, 0x00, 0x00, 0x0c, 57 | 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 59 | 0x4d, 0x79, 0x20, 0x54, 60 | 0x6f, 0x70, 0x20, 0x52, 61 | 0x61, 0x74, 0x65, 0x64 62 | }; 63 | 64 | @Test 65 | public void testHohmUtf16PlaylistTitle() throws IOException, ItlException 66 | { 67 | Input di = new InputImpl(new ByteArrayInputStream(sampleUtf16Chunk)); 68 | 69 | String type = Util.toString(di.readInt()); 70 | assertEquals("hohm", type); 71 | 72 | int length = di.readInt(); 73 | assertEquals(24, length); 74 | 75 | assertEquals(60, di.readInt()); 76 | assertEquals(0x64, di.readInt()); 77 | 78 | String val = ParseLibrary.readGenericHohm(di); 79 | 80 | assertEquals("90\u2019s Music", val); 81 | } 82 | 83 | @Test 84 | public void testHohmLatin1PlaylistTitle() throws IOException, ItlException 85 | { 86 | Input di = new InputImpl(new ByteArrayInputStream(sampleLatin1Chunk)); 87 | 88 | String type = Util.toString(di.readInt()); 89 | assertEquals("hohm", type); 90 | 91 | int length = di.readInt(); 92 | assertEquals(24, length); 93 | 94 | assertEquals(52, di.readInt()); 95 | assertEquals(0x64, di.readInt()); 96 | 97 | String val = ParseLibrary.readGenericHohm(di); 98 | 99 | assertEquals("My Top Rated", val); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Library.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.Collections; 24 | 25 | /** 26 | * A high-level view of a library, including all the data extracted. 27 | */ 28 | public class Library 29 | { 30 | final Hdfm hdr; 31 | private final String path; 32 | private final Collection playlists; 33 | private final Collection podcasts; 34 | private final Collection tracks; 35 | private final Collection artwork; 36 | 37 | public Library(Hdfm header, String path, Collection playlists, Collection podcasts, Collection tracks, 38 | Collection artwork) 39 | { 40 | this.hdr = header; 41 | this.path = path; 42 | this.playlists = playlists; 43 | this.podcasts = podcasts; 44 | this.tracks = tracks; 45 | this.artwork = artwork; 46 | } 47 | 48 | public String getVersion() 49 | { 50 | return hdr.version; 51 | } 52 | 53 | public String getMusicFolder() 54 | { 55 | return path; 56 | } 57 | 58 | public byte[] getLibraryPersistentId() 59 | { 60 | int i = 0; 61 | 62 | while (hdr.headerRemainder[i] == 0x00) { 63 | i++; 64 | } 65 | 66 | /* The ID comes after the first non-zero byte */ 67 | i++; 68 | 69 | byte[] lpid = new byte[8]; 70 | System.arraycopy(hdr.headerRemainder, i, lpid, 0, lpid.length); 71 | return lpid; 72 | } 73 | 74 | public Collection getPlaylistNames() 75 | { 76 | Collection titles = new ArrayList(playlists.size()); 77 | 78 | for (Playlist pl : playlists) 79 | { 80 | String title = pl.getTitle(); 81 | if(title != null) { 82 | titles.add(title); 83 | } 84 | } 85 | 86 | return titles; 87 | } 88 | 89 | private static Collection copy(Collection coll) 90 | { 91 | return Collections.unmodifiableCollection(new ArrayList(coll)); 92 | } 93 | 94 | public Collection getPlaylists() 95 | { 96 | return copy(playlists); 97 | } 98 | 99 | public Collection getTracks() 100 | { 101 | return copy(tracks); 102 | } 103 | 104 | public Collection getPodcasts() 105 | { 106 | return copy(podcasts); 107 | } 108 | 109 | public Collection getArtwork() 110 | { 111 | return copy(artwork); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/tools/ShowAllArtwork.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import java.awt.Dimension; 22 | import java.awt.image.BufferedImage; 23 | import java.io.ByteArrayInputStream; 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.util.Collection; 27 | import java.util.Random; 28 | import java.util.logging.Logger; 29 | 30 | import javax.imageio.ImageIO; 31 | import javax.swing.JFrame; 32 | import javax.swing.JPanel; 33 | import javax.swing.WindowConstants; 34 | 35 | import org.kafsemo.titl.art.AlbumArtworkDirectory; 36 | import org.kafsemo.titl.art.ExtractArt; 37 | 38 | /** 39 | * Simple hack to display all iTunes artwork in a rapid collage. 40 | * 41 | * @author Joseph 42 | */ 43 | public class ShowAllArtwork 44 | { 45 | private static final Logger log = Logger.getLogger(ShowAllArtwork.class.getName()); 46 | 47 | public static void main(String[] args) throws IOException, InterruptedException 48 | { 49 | if (args.length != 1) { 50 | System.err.println("Usage: ShowAllArtwork "); 51 | System.exit(5); 52 | } 53 | 54 | String iTunesDirectory = args[0]; 55 | 56 | JFrame jf = new JFrame(); 57 | JPanel jp = new JPanel(); 58 | jp.setPreferredSize(new Dimension(500, 500)); 59 | jf.getContentPane().add(jp); 60 | 61 | jf.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 62 | jf.pack(); 63 | jf.setVisible(true); 64 | 65 | AlbumArtworkDirectory aad = new AlbumArtworkDirectory(new File(iTunesDirectory, "Album Artwork")); 66 | 67 | Random r = new Random(); 68 | 69 | for (File f : aad) { 70 | if (!f.isFile()) { 71 | continue; 72 | } 73 | Collection streams = ExtractArt.extract(f); 74 | 75 | for (byte[] ba : streams) { 76 | BufferedImage img = ImageIO.read(new ByteArrayInputStream(ba)); 77 | 78 | if(img == null) { 79 | log.warning("Unable to load file: " + f); 80 | continue; 81 | } 82 | 83 | Dimension d = jp.getSize(); 84 | 85 | int x, y; 86 | 87 | if (img.getWidth() < d.width && img.getHeight() < d.height) { 88 | x = r.nextInt(d.width - img.getWidth()); 89 | y = r.nextInt(d.height - img.getHeight()); 90 | } else { 91 | x = (d.width - img.getWidth()) / 2; 92 | y = (d.height - img.getHeight()) / 2; 93 | } 94 | 95 | jp.getGraphics().drawImage(img, x, y, null); 96 | Thread.sleep(100); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/art/ExtractArt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.art; 20 | 21 | import java.awt.image.BufferedImage; 22 | import java.io.ByteArrayInputStream; 23 | import java.io.File; 24 | import java.io.FileInputStream; 25 | import java.io.FileNotFoundException; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | 31 | import javax.imageio.ImageIO; 32 | import javax.swing.ImageIcon; 33 | import javax.swing.JButton; 34 | import javax.swing.JFrame; 35 | import javax.swing.WindowConstants; 36 | 37 | import org.kafsemo.titl.Input; 38 | import org.kafsemo.titl.InputImpl; 39 | import org.kafsemo.titl.Util; 40 | 41 | /** 42 | * A class to extract image data from an .itc2 file. 43 | * Minimal implementation using notes from this article. 44 | * 45 | * @author Joseph 46 | */ 47 | public class ExtractArt 48 | { 49 | public static void main(String[] args) throws FileNotFoundException, IOException 50 | { 51 | File f = new File("/tmp/sample.itc2"); 52 | 53 | Collection streams = extract(f); 54 | 55 | // int i = 0; 56 | 57 | for (byte[] ba : streams) { 58 | BufferedImage img = ImageIO.read(new ByteArrayInputStream(ba)); 59 | 60 | JFrame jf = new JFrame(); 61 | jf.getContentPane().add(new JButton(new ImageIcon(img))); 62 | jf.pack(); 63 | jf.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 64 | jf.setVisible(true); 65 | // OutputStream out = new FileOutputStream("sample" + i + ".png"); 66 | // out.write(ba); 67 | // out.close(); 68 | // i++; 69 | } 70 | } 71 | 72 | public static Collection extract(File f) throws IOException 73 | { 74 | Collection streams = new ArrayList(); 75 | 76 | int remaining = (int) f.length(); 77 | 78 | InputStream in = new FileInputStream(f); 79 | try { 80 | Input di = new InputImpl(in); 81 | while (remaining > 0) { 82 | int bl = di.readInt(); 83 | String type = Util.toString(di.readInt()); 84 | 85 | if(type.equals("item")) { 86 | int ltd = di.readInt(); 87 | di.skipBytes(ltd - 12); 88 | 89 | byte[] ba = new byte[bl - ltd]; 90 | di.readFully(ba); 91 | 92 | streams.add(ba); 93 | } else { 94 | di.skipBytes(bl - 8); 95 | } 96 | 97 | remaining -= bl; 98 | } 99 | } finally { 100 | in.close(); 101 | } 102 | 103 | return streams; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/HohmPodcast.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.IOException; 22 | import java.io.UnsupportedEncodingException; 23 | 24 | public class HohmPodcast 25 | { 26 | public final String url; 27 | public final String link; 28 | public final String aurl; 29 | 30 | public HohmPodcast(String url, String link, String aurl) 31 | { 32 | this.url = url; 33 | this.link = link; 34 | this.aurl = aurl; 35 | } 36 | 37 | public static String toString(byte[] ba) throws UnsupportedEncodingException 38 | { 39 | int lnz = 0; 40 | for (int i = 0; i < ba.length; i++) { 41 | if (ba[i] != 0) { 42 | lnz = i + 1; 43 | } 44 | } 45 | 46 | return new String(ba, 0, lnz, "utf-8"); 47 | } 48 | 49 | public static HohmPodcast parse(Input di, int length) throws IOException 50 | { 51 | String url, link, aurl; 52 | 53 | di.skipBytes(23 * 4); 54 | length -= 23 * 4; 55 | 56 | if (length <= 0) { 57 | return null; 58 | } 59 | 60 | int len = di.readInt(); 61 | String type = Util.toString(di.readInt()); 62 | len -= 8; 63 | Util.assertEquals("url ", type); 64 | 65 | di.skipBytes(12); 66 | len -= 12; 67 | 68 | byte[] ba; 69 | 70 | ba = new byte[len]; 71 | di.readFully(ba); 72 | 73 | url = toString(ba); 74 | 75 | di.skipBytes(15 * 4); 76 | 77 | len = di.readInt(); 78 | type = Util.toString(di.readInt()); 79 | len -= 8; 80 | 81 | Util.assertEquals("link", type); 82 | 83 | di.skipBytes(12); 84 | len -= 12; 85 | 86 | ba = new byte[len]; 87 | di.readFully(ba); 88 | link = toString(ba); 89 | 90 | len = di.readInt(); 91 | type = Util.toString(di.readInt()); 92 | len -= 8; 93 | 94 | while(!"aurl".equals(type)) { 95 | System.out.println(type); 96 | 97 | if(type.equals("pech")) { 98 | System.out.println("Recurse"); 99 | 100 | dump(di, len); 101 | } else { 102 | ba = new byte[len]; 103 | di.readFully(ba); 104 | System.out.println(new String(ba)); 105 | } 106 | 107 | len = di.readInt(); 108 | type = Util.toString(di.readInt()); 109 | len -= 8; 110 | } 111 | 112 | Util.assertEquals("aurl", type); 113 | 114 | di.skipBytes(12); 115 | len -= 12; 116 | 117 | ba = new byte[len]; 118 | di.readFully(ba); 119 | 120 | aurl = toString(ba); 121 | 122 | return new HohmPodcast(url, link, aurl); 123 | } 124 | 125 | static void dump(Input di, int length) throws IOException 126 | { 127 | di.skipBytes(12); 128 | 129 | String type = Util.toString(di.readInt()); 130 | System.out.println(type); 131 | 132 | // parse(di, length - 60 + 23); 133 | // di.skipBytes(24); 134 | // length -= 24; 135 | 136 | // ParseLibrary.hexDumpBytes(di, length); 137 | while(length > 0) { 138 | int len = di.readInt(); 139 | type = Util.toString(di.readInt()); 140 | 141 | System.out.println(type + ", " + len); 142 | 143 | if(type.equals("strt")) { 144 | } else if(type.equals("pech")) { 145 | // dump(di, len - 8); 146 | ParseLibrary.hexDumpBytes(di, length); 147 | } else { 148 | len -= 8; 149 | 150 | // di.skipBytes(12); 151 | // len -= 12; 152 | byte[] ba = new byte[len]; 153 | di.readFully(ba); 154 | System.out.println(new String(ba)); 155 | 156 | length -= len; 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/tools/MoveMusic.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import java.io.File; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.io.OutputStream; 25 | 26 | import org.kafsemo.titl.ItlException; 27 | import org.kafsemo.titl.ProcessLibrary; 28 | 29 | /** 30 | *

A command-line tool to process a music library and move all music files from one directory 31 | * to another. If you move the files on disk at the same time, processing with this tool 32 | * will make the app pick them up from the new location.

33 | * 34 | *

The --use-urls switch will convert the URL rather than 35 | * the filename of each track. On Windows, the default (filename) works.

36 | * 37 | * @author Joseph 38 | */ 39 | public class MoveMusic implements ProcessLibrary.StringConverter 40 | { 41 | private final String libraryFilename; 42 | private final String origDir, destDir; 43 | private boolean useUrls; 44 | 45 | public MoveMusic(String libraryFilename, String string, String string2) 46 | { 47 | this.libraryFilename = libraryFilename; 48 | origDir = string; 49 | destDir = string2; 50 | useUrls = false; 51 | } 52 | 53 | public static MoveMusic fromArgs(String[] args) 54 | { 55 | if (args.length < 3) { 56 | return null; 57 | } 58 | 59 | boolean useUrls = false; 60 | int firstPath; 61 | 62 | if (args[0].startsWith("--")) { 63 | firstPath = 1; 64 | if (args[0].equals("--use-urls")) { 65 | useUrls = true; 66 | } else { 67 | return null; 68 | } 69 | } else { 70 | firstPath = 0; 71 | } 72 | 73 | if (args.length - firstPath != 3) { 74 | return null; 75 | } 76 | 77 | MoveMusic mm = new MoveMusic(args[firstPath], args[firstPath + 1], args[firstPath + 2]); 78 | mm.setUseUrls(useUrls); 79 | return mm; 80 | } 81 | 82 | String getLibraryFilename() 83 | { 84 | return libraryFilename; 85 | } 86 | 87 | String getOrigDir() 88 | { 89 | return origDir; 90 | } 91 | 92 | String getDestDir() 93 | { 94 | return destDir; 95 | } 96 | 97 | boolean isUseUrls() 98 | { 99 | return useUrls; 100 | } 101 | 102 | void setUseUrls(boolean u) 103 | { 104 | useUrls = u; 105 | } 106 | 107 | public static void main(String[] args) throws IOException, ItlException 108 | { 109 | MoveMusic mm = fromArgs(args); 110 | if (mm == null) { 111 | System.err.println("Usage: MoveMusic [--use-urls] "); 112 | System.exit(5); 113 | return; // Unreachable; keep Eclipse NPE detection happy 114 | } 115 | 116 | /* Warn if the paths are specified differently */ 117 | if (hasSlash(mm.origDir) != hasSlash(mm.destDir)) { 118 | System.err.println("Inconsistent paths - one ends with a slash and one doesn't"); 119 | } 120 | 121 | ProcessLibrary pl = new ProcessLibrary(); 122 | if (mm.isUseUrls()) { 123 | pl.register(0x0B, mm); 124 | } else { 125 | pl.register(0x0D, mm); 126 | } 127 | 128 | File f = new File(mm.getLibraryFilename()); 129 | 130 | File f2 = new File(f.getParentFile(), f.getName() + ".processed"); 131 | if (f2.exists()) { 132 | System.err.println("Target file already exists - exiting"); 133 | System.exit(10); 134 | } 135 | 136 | OutputStream out = new FileOutputStream(f2); 137 | try { 138 | pl.process(f, out); 139 | } finally { 140 | out.close(); 141 | } 142 | } 143 | 144 | private static boolean hasSlash(String s) 145 | { 146 | return s.endsWith("/") || s.endsWith("\\"); 147 | } 148 | 149 | public String convert(String s) 150 | { 151 | if (s.startsWith(origDir)) { 152 | String np = destDir + s.substring(origDir.length()); 153 | System.out.println(s + " -> " + np); 154 | return np; 155 | } else { 156 | return s; 157 | } 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/art/AlbumArtworkDirectory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.art; 20 | 21 | import java.io.File; 22 | import java.net.URI; 23 | import java.util.ArrayList; 24 | import java.util.Collection; 25 | import java.util.Collections; 26 | import java.util.HashSet; 27 | import java.util.Iterator; 28 | import java.util.List; 29 | import java.util.Set; 30 | 31 | import org.kafsemo.titl.Artwork; 32 | import org.kafsemo.titl.Library; 33 | import org.kafsemo.titl.Track; 34 | 35 | /** 36 | * Discovers all the files in the artwork directory. 37 | */ 38 | public class AlbumArtworkDirectory implements Iterable 39 | { 40 | private final File dir; 41 | private Set names; 42 | 43 | private final Set used = new HashSet(); 44 | 45 | public AlbumArtworkDirectory(String dirName) 46 | { 47 | this(new File(dirName)); 48 | } 49 | 50 | public AlbumArtworkDirectory(File d) 51 | { 52 | this.dir = d; 53 | names = null; 54 | } 55 | 56 | private void scan() 57 | { 58 | Set names = new HashSet(); 59 | 60 | Collection files = new ArrayList(); 61 | recurse(dir, files); 62 | 63 | URI base = dir.toURI(); 64 | 65 | for (File f : files) { 66 | String relPath = base.relativize(f.toURI()).toString(); 67 | names.add(relPath); 68 | } 69 | 70 | this.names = names; 71 | } 72 | 73 | private Set getNames() 74 | { 75 | if (names == null) { 76 | scan(); 77 | } 78 | 79 | return names; 80 | } 81 | 82 | private void recurse(File f, Collection files) 83 | { 84 | if (f.isFile()) { 85 | files.add(f); 86 | } else if (f.isDirectory()) { 87 | File[] fa = f.listFiles(); 88 | if (fa != null) { 89 | for (File sf : fa) { 90 | recurse(sf, files); 91 | } 92 | } 93 | } 94 | } 95 | 96 | @Override 97 | public Iterator iterator() 98 | { 99 | return asFiles(getNames()).iterator(); 100 | } 101 | 102 | private Iterable asFiles(Iterable names) 103 | { 104 | Collection files = new ArrayList(); 105 | for (String n : names) { 106 | files.add(new File(dir, n)); 107 | } 108 | return Collections.unmodifiableCollection(files); 109 | } 110 | 111 | public Iterable getUnused() 112 | { 113 | Collection c = new HashSet(getNames()); 114 | c.removeAll(used); 115 | 116 | List inOrder = new ArrayList(c); 117 | Collections.sort(inOrder); 118 | return asFiles(inOrder); 119 | } 120 | 121 | public File getCache(Library l, Artwork art) 122 | { 123 | byte[] artId = art.getPersistentId(); 124 | 125 | return get(l, ArtworkFile.Directory.Cache, artId); 126 | } 127 | 128 | public File getDownload(Library l, Track t) 129 | { 130 | byte[] artId = t.getAlbumPersistentId(); 131 | 132 | get(l, ArtworkFile.Directory.Cache, artId); 133 | return get(l, ArtworkFile.Directory.Download, artId); 134 | } 135 | 136 | public File get(Library l, Track t) 137 | { 138 | byte[] artId = t.getAlbumPersistentId(); 139 | 140 | File f = get(l, ArtworkFile.Directory.Download, artId); 141 | if (f != null && f.isFile()) { 142 | return f; 143 | } else { 144 | return get(l, ArtworkFile.Directory.Cache, artId); 145 | } 146 | } 147 | 148 | public File get(Library l, ArtworkFile.Directory type, byte[] artId) 149 | { 150 | if (artId == null) { 151 | return null; 152 | } 153 | 154 | ArtworkFile af = new ArtworkFile(type, artId, 2); 155 | 156 | String f = af.toString(l.getLibraryPersistentId()); 157 | 158 | // ArtworkFile af1 = new ArtworkFile(type, artId, 1); 159 | // 160 | // String f1 = af1.toString(l.getLibraryPersistentId()); 161 | // 162 | // if (getNames().contains(f1)) { 163 | // f = f1; 164 | // } else { 165 | // f = null; 166 | // return null; 167 | // } 168 | 169 | used.add(f); 170 | 171 | return new File(dir, f); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /titl-rdf/src/main/java/org/kafsemo/titl/rdf/LibraryAsRdf.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.rdf; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.text.DateFormat; 24 | import java.text.SimpleDateFormat; 25 | import java.util.Date; 26 | import java.util.TimeZone; 27 | 28 | import org.kafsemo.titl.ItlException; 29 | import org.kafsemo.titl.Library; 30 | import org.kafsemo.titl.ParseLibrary; 31 | import org.kafsemo.titl.Track; 32 | import org.kafsemo.titl.Util; 33 | import org.kafsemo.titl.art.AlbumArtworkDirectory; 34 | import org.kafsemo.titl.tools.ArtworkWebPage; 35 | import org.openrdf.model.Resource; 36 | import org.openrdf.model.URI; 37 | import org.openrdf.model.impl.LiteralImpl; 38 | import org.openrdf.model.impl.StatementImpl; 39 | import org.openrdf.model.impl.URIImpl; 40 | import org.openrdf.rio.RDFHandlerException; 41 | import org.openrdf.rio.RDFWriter; 42 | import org.openrdf.rio.turtle.TurtleWriterFactory; 43 | 44 | public class LibraryAsRdf 45 | { 46 | static URI nid3(String s) 47 | { 48 | return new URIImpl("http://www.semanticdesktop.org/ontologies/2007/05/10/nid3/#" + s); 49 | } 50 | 51 | private static final URI TYPE = new URIImpl("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); 52 | private static final URI AUDIO = new URIImpl("http://www.semanticdesktop.org/ontologies/nfo/#Audio"); 53 | 54 | private static final URI NID3_TITLE = nid3("title"), 55 | NID3_ALBUM_TITLE = nid3("albumTitle"), 56 | NID3_LEAD_ARTIST = nid3("leadArtist"), 57 | NID3_ATTACHED_PICTURE = nid3("attachedPicture"); 58 | 59 | static URI titl(String s) 60 | { 61 | return new URIImpl("tag:kafsemo.org,2011:titl-rdf/" + s); 62 | } 63 | 64 | private static final URI ITUNES_PLAY_COUNT = titl("Play Count"); 65 | private static final URI ITUNES_PLAY_DATE_UTC = titl("Play Date UTC"); 66 | private static final URI ITUNES_PERSISTENT_ID = titl("Persistent ID"); 67 | 68 | 69 | public static void main(String[] args) throws RDFHandlerException, IOException, ItlException 70 | { 71 | if (args.length != 1 && args.length != 2) { 72 | System.err.println("Usage: LibraryAsRdf [artwork output directory]"); 73 | System.exit(5); 74 | } 75 | 76 | String filename = args[0]; 77 | 78 | File artworkOutput; 79 | if (args.length == 2) { 80 | artworkOutput = new File(args[1]); 81 | } else { 82 | artworkOutput = null; 83 | } 84 | 85 | File libFile = new File(filename); 86 | 87 | Library lib = ParseLibrary.parse(libFile); 88 | 89 | AlbumArtworkDirectory artDir = new AlbumArtworkDirectory(ArtworkWebPage.artworkDirectoryFor(libFile)); 90 | ArtworkWebPage awp = new ArtworkWebPage(); 91 | 92 | RDFWriter rw = new TurtleWriterFactory().getWriter(System.out); 93 | 94 | rw.handleNamespace("nfo", "http://www.semanticdesktop.org/ontologies/nfo/#"); 95 | rw.handleNamespace("nid3", nid3("").toString()); 96 | 97 | rw.startRDF(); 98 | 99 | 100 | // int count = 0; 101 | 102 | DateFormat df = utcDateFormat(); 103 | 104 | for (Track t : lib.getTracks()) { 105 | // if (count++ > 10) 106 | // break; 107 | 108 | Resource res = new URIImpl(t.getLocalUrl()); 109 | 110 | rw.handleStatement(new StatementImpl(res, TYPE, AUDIO)); 111 | 112 | String artist = t.getArtist(); 113 | String album = t.getAlbum(); 114 | // Track index? 115 | String title = t.getName(); 116 | 117 | if (artist != null) { 118 | rw.handleStatement(new StatementImpl(res, NID3_LEAD_ARTIST, new LiteralImpl(artist))); 119 | } 120 | 121 | if (album != null) { 122 | rw.handleStatement(new StatementImpl(res, NID3_ALBUM_TITLE, new LiteralImpl(album))); 123 | } 124 | 125 | if (title != null) { 126 | rw.handleStatement(new StatementImpl(res, NID3_TITLE, new LiteralImpl(title))); 127 | } 128 | 129 | int playCount = t.getPlayCount(); 130 | Date lastPlayDate = t.getLastPlayDate(); 131 | byte[] persistentId = t.getPersistentId(); 132 | 133 | rw.handleStatement(new StatementImpl(res, ITUNES_PLAY_COUNT, new LiteralImpl(Integer.toString(playCount)))); 134 | 135 | if (lastPlayDate != null) { 136 | rw.handleStatement(new StatementImpl(res, ITUNES_PLAY_DATE_UTC, new LiteralImpl(df.format(lastPlayDate)))); 137 | } 138 | 139 | if (persistentId != null) { 140 | rw.handleStatement(new StatementImpl(res, ITUNES_PERSISTENT_ID, new LiteralImpl(Util.pidToString(persistentId)))); 141 | } 142 | 143 | if (artworkOutput != null) { 144 | File f = artDir.getDownload(lib, t); 145 | for (String artFile : awp.writeAsGfxFiles(artworkOutput, f)) { 146 | String artFileUri = new File(artworkOutput, artFile).toURI().toASCIIString(); 147 | rw.handleStatement(new StatementImpl(res, NID3_ATTACHED_PICTURE, new URIImpl(artFileUri))); 148 | } 149 | } 150 | } 151 | rw.endRDF(); 152 | } 153 | 154 | private static DateFormat utcDateFormat() 155 | { 156 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 157 | df.setTimeZone(TimeZone.getTimeZone("UTC")); 158 | return df; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestHdfm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertArrayEquals; 22 | import static org.junit.Assert.assertEquals; 23 | import static org.junit.Assert.assertFalse; 24 | import static org.junit.Assert.assertNotNull; 25 | 26 | import java.io.ByteArrayInputStream; 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.OutputStream; 29 | import java.util.Arrays; 30 | import java.util.zip.DeflaterOutputStream; 31 | import java.util.zip.ZipException; 32 | 33 | import org.junit.Test; 34 | 35 | public class TestHdfm 36 | { 37 | private static final byte[] hdfm6Header = { 38 | 0x68, 0x64, 0x66, 0x6d, 39 | 0x00, 0x00, 0x00, (byte) 0x84, 40 | 0x00, 0x68, 0x65, 0x7c, 41 | 0x00, 0x08, 0x00, 0x03, 42 | 0x05, 0x36, 0x2e, 0x30, 43 | 0x2e, 0x34, 0x00, 0x00, 44 | 0x00, 0x00, 0x00, 0x00, 45 | 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x03, 51 | (byte) 0xb9, 0x18, 0x5c, (byte) 0xfd, 52 | (byte) 0xbd, (byte) 0xbc, 0x46, 0x06, 53 | 0x00, 0x00, 0x00, 0x09, 54 | 0x02, 0x01, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 56 | 0x00, 0x00, 0x00, 0x00, 57 | 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 59 | 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 61 | 0x00, 0x00, 0x00, 0x00, 62 | 0x00, 0x00, 0x00, 0x00, 63 | 0x00, 0x00, 0x00, 0x00, 64 | 0x00, 0x00, 0x00, 0x00, 65 | 0x00, 0x00, 0x00, 0x00, 66 | 0x00, 0x00, 0x00, 0x00, 67 | 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 69 | 0x00, 0x00, 0x00, 0x00, 70 | 0x00, 0x00, 0x00, 0x00 71 | }; 72 | 73 | private static byte[] hdfm8Header = { 74 | 0x68, 0x64, 0x66, 0x6d, 75 | 0x00, 0x00, 0x00, (byte) 0x84, 76 | 0x00, 0x00, (byte) 0xed, 0x1b, 77 | 0x00, 0x0e, 0x00, 0x01, 78 | 0x03, 0x38, 0x2e, 0x30, 79 | 0x00, 0x00, 0x00, 0x00, 80 | 0x00, 0x00, 0x00, 0x00, 81 | 0x00, 0x00, 0x00, 0x00, 82 | 0x00, 0x00, 0x00, 0x00, 83 | 0x00, 0x00, 0x00, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 85 | 0x00, 0x00, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0x06, 87 | (byte) 0xef, (byte) 0x96, 0x71, (byte) 0xac, 88 | 0x74, 0x72, (byte) 0xd9, 0x7b, 89 | 0x00, 0x00, 0x00, 0x22, 90 | 0x02, 0x01, 0x00, 0x00, 91 | 0x00, 0x00, 0x00, 0x00, 92 | 0x00, 0x00, 0x00, 0x10, 93 | 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x01, 0x00, 0x00, 95 | 0x00, 0x00, 0x00, 0x00, 96 | 0x00, 0x00, 0x00, 0x00, 97 | 0x00, 0x00, 0x00, 0x00, 98 | 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x00, 0x00, 102 | 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x00, 0x00, 0x00, 104 | 0x00, 0x00, 0x00, 0x00, 105 | 0x00, 0x00, 0x00, 0x00, 106 | 0x00, 0x00, 0x00, 0x00, 107 | }; 108 | 109 | @Test 110 | public void testParse6HdfmHeader() throws Exception 111 | { 112 | byte[] wholeFile = new byte[6841724]; 113 | System.arraycopy(hdfm6Header, 0, wholeFile, 0, hdfm6Header.length); 114 | 115 | Input di = new InputImpl(new ByteArrayInputStream(wholeFile)); 116 | 117 | Hdfm hdfm = Hdfm.read(di, wholeFile.length); 118 | assertNotNull(hdfm); 119 | assertEquals("6.0.4", hdfm.version); 120 | assertEquals(wholeFile.length - hdfm6Header.length, hdfm.fileData.length); 121 | } 122 | 123 | @Test 124 | public void testParse8HdfmHeader() throws Exception 125 | { 126 | byte[] wholeFile = new byte[60699]; 127 | System.arraycopy(hdfm8Header, 0, wholeFile, 0, hdfm8Header.length); 128 | 129 | Input di = new InputImpl(new ByteArrayInputStream(wholeFile)); 130 | 131 | Hdfm hdfm = Hdfm.read(di, wholeFile.length); 132 | assertNotNull(hdfm); 133 | assertEquals("8.0", hdfm.version); 134 | assertEquals(wholeFile.length - hdfm8Header.length, hdfm.fileData.length); 135 | } 136 | 137 | @Test 138 | public void uncompressedDataIsNotInflated() throws Exception 139 | { 140 | byte[] data = "Uncompressed.".getBytes("us-ascii"); 141 | 142 | assertEquals("The same array is returned when data is not compressed", 143 | data, Hdfm.inflate(data)); 144 | } 145 | 146 | @Test 147 | public void compressedDataIsInflated() throws Exception 148 | { 149 | byte[] data = "Uncompressed.".getBytes("us-ascii"); 150 | 151 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 152 | OutputStream out = new DeflaterOutputStream(baos); 153 | out.write(data); 154 | out.close(); 155 | 156 | byte[] compressed = baos.toByteArray(); 157 | 158 | assertFalse(Arrays.equals(data, compressed)); 159 | 160 | byte[] after = Hdfm.inflate(compressed); 161 | 162 | assertArrayEquals("Data is decompressed", data, after); 163 | } 164 | 165 | @Test(expected = ZipException.class) 166 | public void probablyCompressedFileThrowsExceptionIfErrorDuringDecompression() throws Exception 167 | { 168 | byte[] data = "Uncompressed.".getBytes("us-ascii"); 169 | 170 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 171 | OutputStream out = new DeflaterOutputStream(baos); 172 | out.write(data); 173 | out.close(); 174 | 175 | byte[] compressed = baos.toByteArray(); 176 | 177 | /* Intentionally corrupt the compressed data */ 178 | compressed[compressed.length - 1] ^= 0xFF; 179 | Hdfm.inflate(compressed); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/ProcessLibrary.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.ByteArrayInputStream; 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.DataInput; 24 | import java.io.DataInputStream; 25 | import java.io.DataOutput; 26 | import java.io.DataOutputStream; 27 | import java.io.File; 28 | import java.io.FileInputStream; 29 | import java.io.IOException; 30 | import java.io.InputStream; 31 | import java.io.OutputStream; 32 | import java.util.HashMap; 33 | import java.util.Map; 34 | 35 | /** 36 | * A processor that can be configured with callbacks to modify any particular string field. 37 | * 38 | * @author Joseph 39 | */ 40 | public class ProcessLibrary 41 | { 42 | private final Map converters = new HashMap(); 43 | 44 | public void process(File inFile, OutputStream outStr) throws IOException, ItlException 45 | { 46 | /* Read the original library in */ 47 | Hdfm hdfm; 48 | 49 | InputStream inStr = new FileInputStream(inFile); 50 | try { 51 | Input di = new InputImpl(inStr); 52 | hdfm = Hdfm.read(di, inFile.length()); 53 | } finally { 54 | inStr.close(); 55 | } 56 | 57 | /* Modify... */ 58 | ByteArrayOutputStream dto = new ByteArrayOutputStream(); 59 | 60 | process(new DataInputStream(new ByteArrayInputStream(hdfm.fileData)), hdfm.fileData.length, new DataOutputStream(dto)); 61 | 62 | /* ...and write out */ 63 | DataOutput out = new DataOutputStream(outStr); 64 | 65 | hdfm.write(out, dto.toByteArray()); 66 | } 67 | 68 | void process(DataInput di, int totalLength, DataOutput out) throws IOException, ItlException 69 | { 70 | int remaining = totalLength; 71 | 72 | boolean going = true; 73 | 74 | while(going) 75 | { 76 | int consumed = 0; 77 | String type = Util.toString(di.readInt()); 78 | consumed += 4; 79 | 80 | int length = di.readInt(); 81 | consumed += 4; 82 | 83 | if(type.equals("hohm")) { 84 | int recLength = di.readInt(); 85 | consumed += 4; 86 | 87 | Integer hohmType = Integer.valueOf(di.readInt()); 88 | consumed += 4; 89 | 90 | byte[] ba = new byte[recLength - consumed]; 91 | di.readFully(ba); 92 | 93 | StringConverter sc = converters.get(hohmType); 94 | if (sc != null) { 95 | ba = mapHohm(new DataInputStream(new ByteArrayInputStream(ba)), sc); 96 | } 97 | 98 | /* Write out again */ 99 | out.writeInt(Util.fromString(type)); 100 | out.writeInt(length); 101 | 102 | out.writeInt(ba.length + consumed); 103 | out.writeInt(hohmType); 104 | 105 | out.write(ba); 106 | 107 | remaining -= (recLength); 108 | 109 | } else { 110 | byte[] ba = new byte[length - consumed]; 111 | 112 | di.readFully(ba); 113 | 114 | /* Did we hit the end? */ 115 | if(type.equals("hdsm")) { 116 | going = !ParseLibrary.readHdsm(new InputImpl(new ByteArrayInputStream(ba)), ba.length); 117 | } 118 | 119 | remaining -= length; 120 | 121 | /* Write out again */ 122 | out.writeInt(Util.fromString(type)); 123 | out.writeInt(ba.length + 8); 124 | out.write(ba); 125 | } 126 | } 127 | 128 | byte[] footerBytes = new byte[remaining]; 129 | di.readFully(footerBytes); 130 | 131 | // String footer = new String(footerBytes, "iso-8859-1"); 132 | // System.out.println("Footer: " + footer); 133 | 134 | out.write(footerBytes); 135 | } 136 | 137 | static byte[] mapHohm(DataInput di, StringConverter sc) throws IOException, ItlException 138 | { 139 | /* Read in */ 140 | byte[] unknown = new byte[12]; 141 | di.readFully(unknown); 142 | 143 | int dataLength = di.readInt(); 144 | byte[] alsoUnknown = new byte[8]; 145 | di.readFully(alsoUnknown); 146 | 147 | byte[] data = new byte[dataLength]; 148 | di.readFully(data); 149 | 150 | String s = ParseLibrary.toString(data, unknown[11]); 151 | 152 | s = sc.convert(s); 153 | 154 | /* Write out */ 155 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 156 | DataOutput out = new DataOutputStream(baos); 157 | 158 | /* Choose the necessary character encoding */ 159 | Encoding enc = chooseEncoding(s); 160 | 161 | unknown[11] = enc.code; 162 | byte[] newData = s.getBytes(enc.name); 163 | out.write(unknown); 164 | out.writeInt(newData.length); 165 | out.write(alsoUnknown); 166 | out.write(newData); 167 | 168 | return baos.toByteArray(); 169 | } 170 | 171 | static Encoding chooseEncoding(String s) 172 | { 173 | for (char c : s.toCharArray()) { 174 | if (c > 0xff) { 175 | return Encoding.UTF16BE; 176 | } 177 | } 178 | 179 | return Encoding.ISO88591; 180 | } 181 | 182 | /** 183 | * Register a converter for a particular HOHM type. 184 | * 185 | *
    186 | *
  • 0x02 - Track title 187 | *
  • 0x0d - Location 188 | *
189 | */ 190 | public void register(int hohmType, StringConverter cnvtr) 191 | { 192 | converters.put(Integer.valueOf(hohmType), cnvtr); 193 | } 194 | 195 | public enum Encoding 196 | { 197 | UNSPECIFIED((byte) 0, "iso-8859-1"), 198 | UTF16BE((byte) 1, "utf-16be"), 199 | ISO88591((byte) 3, "iso-8859-1"); 200 | 201 | private final byte code; 202 | private final String name; 203 | 204 | Encoding(byte c, String n) 205 | { 206 | this.code = c; 207 | this.name = n; 208 | } 209 | } 210 | 211 | public interface StringConverter 212 | { 213 | String convert(String s); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/tools/ArtworkWebPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl.tools; 20 | 21 | import java.io.BufferedWriter; 22 | import java.io.ByteArrayInputStream; 23 | import java.io.File; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import java.io.OutputStream; 27 | import java.io.OutputStreamWriter; 28 | import java.util.ArrayList; 29 | import java.util.Collection; 30 | import java.util.Collections; 31 | import java.util.HashMap; 32 | import java.util.HashSet; 33 | import java.util.Iterator; 34 | import java.util.Map; 35 | import java.util.Set; 36 | 37 | import javax.imageio.ImageIO; 38 | import javax.imageio.ImageReader; 39 | 40 | import org.kafsemo.titl.Artwork; 41 | import org.kafsemo.titl.ItlException; 42 | import org.kafsemo.titl.Library; 43 | import org.kafsemo.titl.ParseLibrary; 44 | import org.kafsemo.titl.Track; 45 | import org.kafsemo.titl.art.AlbumArtworkDirectory; 46 | import org.kafsemo.titl.art.ExtractArt; 47 | 48 | /** 49 | *

Write as much artwork as possible as separate image files 50 | * and create a web page with album titles.

51 | *

The artwork is currently from the iTunes cache, a 52 | * copy of the artwork embedded in the media files.

53 | */ 54 | public class ArtworkWebPage 55 | { 56 | public static void main(String[] args) throws IOException, ItlException 57 | { 58 | if (args.length != 2) 59 | { 60 | System.err.println("Usage: ArtworkWebPage "); 61 | System.exit(5); 62 | } 63 | 64 | File libFile = new File(args[0]); 65 | File outdir = new File(args[1]); 66 | 67 | outdir.mkdir(); 68 | 69 | AlbumArtworkDirectory artDir = new AlbumArtworkDirectory(artworkDirectoryFor(libFile)); 70 | 71 | Library l = ParseLibrary.parse(libFile); 72 | 73 | Collection artwork = l.getArtwork(); 74 | Collection tracks = l.getTracks(); 75 | 76 | BufferedWriter bw = new BufferedWriter( 77 | new OutputStreamWriter( 78 | new FileOutputStream(new File(outdir, "index.html")), "utf-8")); 79 | 80 | ArtworkWebPage awp = new ArtworkWebPage(); 81 | 82 | Set done = new HashSet(); 83 | 84 | try { 85 | bw.write("\n"); 86 | bw.write("\n"); 87 | bw.newLine(); 88 | 89 | bw.write("

Artwork

\n"); 90 | 91 | for (Track art : tracks) { 92 | File f = artDir.getDownload(l, art); 93 | 94 | // XXX Should be escaped 95 | bw.write("

" + art.getName() + "

"); 96 | bw.newLine(); 97 | bw.write("

" + art.getArtist() + "

"); 98 | bw.newLine(); 99 | 100 | if (done.contains(f)) { 101 | bw.write("

(Done.)

"); 102 | bw.newLine(); 103 | } else for (String filename : awp.writeAsGfxFiles(outdir, f)) { 104 | // XXX Should be escaped 105 | bw.write(""); 106 | bw.newLine(); 107 | 108 | done.add(f); 109 | } 110 | } 111 | 112 | bw.write("

Cached

\n"); 113 | 114 | for (Artwork art : artwork) { 115 | // XXX Should be escaped 116 | bw.write("

" + art.title + "

"); 117 | bw.newLine(); 118 | bw.write("

" + art.artist + "

"); 119 | bw.newLine(); 120 | 121 | File f = artDir.getCache(l, art); 122 | for (String filename : awp.writeAsGfxFiles(outdir, f)) { 123 | // XXX Should be escaped 124 | bw.write(""); 125 | bw.newLine(); 126 | } 127 | } 128 | 129 | bw.write("

Other

"); 130 | 131 | for (File f : artDir.getUnused()) { 132 | bw.write("

" + f.getName() + "

\n"); 133 | 134 | for (String filename : awp.writeAsGfxFiles(outdir, f)) { 135 | // XXX Should be escaped 136 | bw.write(""); 137 | bw.newLine(); 138 | } 139 | } 140 | } finally { 141 | bw.close(); 142 | } 143 | } 144 | 145 | public static File artworkDirectoryFor(File libFile) 146 | { 147 | File artDir = new File(libFile.getParentFile(), "Album Artwork"); 148 | return artDir; 149 | } 150 | 151 | Map> written = new HashMap>(); 152 | int imageIndex; 153 | 154 | public Iterable writeAsGfxFiles(File outdir, File f) 155 | throws IOException 156 | { 157 | if (f == null || !f.isFile()) { 158 | return Collections.emptyList(); 159 | } 160 | 161 | Iterable alreadyWritten = written.get(f); 162 | if (alreadyWritten != null) { 163 | return alreadyWritten; 164 | } 165 | 166 | Collection artStreams = ExtractArt.extract(f); 167 | 168 | Collection names = new ArrayList(artStreams.size()); 169 | 170 | for (byte[] ba : artStreams) { 171 | String filename = "image-" + (imageIndex++); 172 | 173 | filename += suffix(ba); 174 | File output = new File(outdir, filename); 175 | OutputStream out = new FileOutputStream(output); 176 | try { 177 | out.write(ba); 178 | 179 | names.add(filename); 180 | } finally { 181 | out.close(); 182 | } 183 | } 184 | 185 | written.put(f, names); 186 | 187 | return names; 188 | } 189 | 190 | static String suffix(byte[] ba) throws IOException 191 | { 192 | ByteArrayInputStream in = new ByteArrayInputStream(ba); 193 | 194 | Iterator x = ImageIO.getImageReaders(ImageIO.createImageInputStream(in)); 195 | if (x.hasNext()) { 196 | return "." + x.next().getFormatName().toLowerCase(); 197 | } else { 198 | return ""; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /LGPL-3: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Track.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.util.Date; 22 | 23 | public class Track 24 | { 25 | private int trackId; 26 | private String name; 27 | private String album; 28 | private String artist; 29 | private String genre; 30 | private String kind; 31 | private Date dateModified; 32 | private int size; 33 | private int totalTime; 34 | private int year; 35 | private Date dateAdded; 36 | private int bitRate; 37 | private int sampleRate; 38 | private String location; 39 | private int rating; 40 | private String url; 41 | private byte[] albumPersistentId; 42 | 43 | public int getTrackId() 44 | { 45 | return trackId; 46 | } 47 | 48 | public String getName() 49 | { 50 | return name; 51 | } 52 | 53 | public String getArtist() 54 | { 55 | return artist; 56 | } 57 | 58 | public String getAlbum() 59 | { 60 | return album; 61 | } 62 | 63 | public String getGenre() 64 | { 65 | return genre; 66 | } 67 | 68 | public String getKind() 69 | { 70 | return kind; 71 | } 72 | 73 | public long getSize() 74 | { 75 | return size; 76 | } 77 | 78 | public int getTotalTime() 79 | { 80 | return totalTime; 81 | } 82 | 83 | public int getYear() 84 | { 85 | return year; 86 | } 87 | 88 | public int getBitRate() 89 | { 90 | return bitRate; 91 | } 92 | 93 | public int getSampleRate() 94 | { 95 | return sampleRate; 96 | } 97 | 98 | public Date getDateModified() 99 | { 100 | return dateModified; 101 | } 102 | 103 | public Date getDateAdded() 104 | { 105 | return dateAdded; 106 | } 107 | 108 | public Date getReleaseDate() { 109 | // TODO Auto-generated method stub 110 | return null; 111 | } 112 | 113 | public int getArtworkCount() { 114 | // TODO Auto-generated method stub 115 | return -1; 116 | } 117 | 118 | public byte[] getPersistentId() { 119 | return persistentId; 120 | } 121 | 122 | public String getTrackType() { 123 | // TODO Auto-generated method stub 124 | return null; 125 | } 126 | 127 | public boolean isPodcast() { 128 | // TODO Auto-generated method stub 129 | return false; 130 | } 131 | 132 | public boolean isUnplayed() { 133 | // TODO Auto-generated method stub 134 | return false; 135 | } 136 | 137 | public String getLocation() 138 | { 139 | return location; 140 | } 141 | 142 | public int getFileFolderCount() { 143 | // TODO Auto-generated method stub 144 | return -1; 145 | } 146 | 147 | public int getLibraryFolderCount() { 148 | // TODO Auto-generated method stub 149 | return -1; 150 | } 151 | 152 | public void setTrackId(int trackId) 153 | { 154 | this.trackId = trackId; 155 | } 156 | 157 | public void setName(String n) 158 | { 159 | this.name = n; 160 | } 161 | 162 | public void setAlbum(String a) 163 | { 164 | this.album = a; 165 | } 166 | 167 | public void setArtist(String artist) 168 | { 169 | this.artist = artist; 170 | } 171 | 172 | public void setGenre(String genre) 173 | { 174 | this.genre = genre; 175 | } 176 | 177 | public void setKind(String kind) 178 | { 179 | this.kind = kind; 180 | } 181 | 182 | public void setDateModified(Date date) 183 | { 184 | this.dateModified = date; 185 | } 186 | 187 | public void setSize(int fileSize) 188 | { 189 | this.size = fileSize; 190 | } 191 | 192 | public void setTotalTime(int playtimeMillis) 193 | { 194 | this.totalTime = playtimeMillis; 195 | } 196 | 197 | public void setYear(int year) 198 | { 199 | this.year = year; 200 | } 201 | 202 | public void setDateAdded(Date date) 203 | { 204 | this.dateAdded = date; 205 | } 206 | 207 | public void setBitRate(int bitRate) 208 | { 209 | this.bitRate = bitRate; 210 | } 211 | 212 | public void setSampleRate(int rate) 213 | { 214 | this.sampleRate = rate; 215 | } 216 | 217 | public void setLocation(String location) 218 | { 219 | this.location = location; 220 | } 221 | 222 | public void setRating(int rating) 223 | { 224 | this.rating = rating; 225 | } 226 | 227 | public int getRating() 228 | { 229 | return rating; 230 | } 231 | 232 | /** 233 | * @return the download URL for a podcast item 234 | */ 235 | public String getUrl() 236 | { 237 | return url; 238 | } 239 | 240 | public void setUrl(String u) 241 | { 242 | this.url = u; 243 | } 244 | 245 | String podcastUrl; 246 | private String itunesKeywords; 247 | private String subtitle; 248 | private String feedLink; 249 | private String localUrl; 250 | private String summary; 251 | private byte[] persistentId; 252 | private int playcount; 253 | private Date playDate; 254 | 255 | public void setPodcastUrl(String url) 256 | { 257 | podcastUrl = url; 258 | } 259 | 260 | public String getItunesKeywords() 261 | { 262 | return itunesKeywords; 263 | } 264 | 265 | public void setItunesKeywords(String kw) 266 | { 267 | this.itunesKeywords = kw; 268 | } 269 | 270 | public String getItunesSubtitle() 271 | { 272 | return subtitle; 273 | } 274 | 275 | public void setItunesSubtitle(String st) 276 | { 277 | this.subtitle = st; 278 | } 279 | 280 | // public String getAuthor() 281 | // { 282 | // return author; 283 | // } 284 | // 285 | // public void setAuthor(String a) 286 | // { 287 | // this.author = a; 288 | // } 289 | 290 | public void setFeedLink(String subtitleOrFeedLink) 291 | { 292 | this.feedLink = subtitleOrFeedLink; 293 | } 294 | 295 | public String getFeedLink() 296 | { 297 | return feedLink; 298 | } 299 | 300 | public String getLocalUrl() 301 | { 302 | return localUrl; 303 | } 304 | 305 | public void setLocalUrl(String url) 306 | { 307 | this.localUrl = url; 308 | } 309 | 310 | public String getItunesSummary() 311 | { 312 | return summary; 313 | } 314 | 315 | public void setItunesSummary(String summary) 316 | { 317 | this.summary = summary; 318 | } 319 | 320 | public void setAlbumPersistentId(byte[] id) 321 | { 322 | this.albumPersistentId = id; 323 | } 324 | 325 | public byte[] getAlbumPersistentId() 326 | { 327 | return albumPersistentId; 328 | } 329 | 330 | public void setPersistentId(byte[] persistentId) 331 | { 332 | this.persistentId = persistentId; 333 | } 334 | 335 | public void setPlayCount(int playcount) 336 | { 337 | this.playcount = playcount; 338 | } 339 | 340 | public int getPlayCount() 341 | { 342 | return this.playcount; 343 | } 344 | 345 | public void setLastPlayDate(Date playDate) 346 | { 347 | if (playDate != null) { 348 | this.playDate = new Date(playDate.getTime()); 349 | } else { 350 | this.playDate = null; 351 | } 352 | } 353 | 354 | public Date getLastPlayDate() 355 | { 356 | return this.playDate; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/Hdfm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | import static org.kafsemo.titl.Util.assertEquals; 21 | 22 | import java.io.ByteArrayInputStream; 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.DataOutput; 25 | import java.io.IOException; 26 | import java.io.UnsupportedEncodingException; 27 | import java.security.GeneralSecurityException; 28 | import java.util.Arrays; 29 | import java.util.zip.Deflater; 30 | import java.util.zip.DeflaterInputStream; 31 | import java.util.zip.Inflater; 32 | import java.util.zip.InflaterInputStream; 33 | import java.util.zip.ZipException; 34 | 35 | import javax.crypto.Cipher; 36 | import javax.crypto.spec.SecretKeySpec; 37 | 38 | public class Hdfm 39 | { 40 | public final String version; 41 | public final int unknown; 42 | final byte[] headerRemainder; 43 | public final byte[] fileData; 44 | public Boolean compressContents; 45 | 46 | private Hdfm(String version, int unknown, byte[] headerRemainder, byte[] fileData, Boolean compressContents) 47 | { 48 | this.version = version; 49 | this.unknown = unknown; 50 | this.headerRemainder = headerRemainder; 51 | this.fileData = fileData; 52 | this.compressContents = compressContents; 53 | } 54 | 55 | // Byte Length Comment 56 | // ----------------------- 57 | // 0 4 'hdfm' 58 | // 4 4 L = header length 59 | // 8 4 file length ? 60 | // 12 4 ? 61 | // 13 1 N = length of version string 62 | // 14 N application version string 63 | // 14+N L-N-17 ? 64 | public static Hdfm read(Input di, long fileLength) throws IOException, ItlException 65 | { 66 | int hdr = di.readInt(); 67 | assertEquals("hdfm", Util.toString(hdr)); 68 | 69 | int hl = di.readInt(); 70 | 71 | int fl = di.readInt(); 72 | if (fileLength != fl) 73 | { 74 | throw new IOException("Disk file is " + fileLength + " but header claims " + fl); 75 | } 76 | 77 | int unknown = di.readInt(); 78 | 79 | int vsl = di.readUnsignedByte(); 80 | byte[] avs = new byte[vsl]; 81 | di.readFully(avs); 82 | 83 | String version = new String(avs, "us-ascii"); 84 | 85 | int consumed = vsl + 17; 86 | 87 | byte[] headerRemainder = new byte[hl - consumed]; 88 | di.readFully(headerRemainder); 89 | 90 | consumed += headerRemainder.length; 91 | 92 | if (hl != consumed) 93 | { 94 | throw new IOException("Header claims to be " + hl + " bytes but read " + consumed); 95 | } 96 | 97 | byte[] restOfFile = new byte[(int)fileLength - consumed]; 98 | 99 | di.readFully(restOfFile); 100 | 101 | byte[] decrypted = new byte[restOfFile.length]; 102 | 103 | /* Decrypt */ 104 | decrypted = crypt(version, restOfFile, Cipher.DECRYPT_MODE); 105 | 106 | /* Unzip (aka inflate, decompress...) */ 107 | byte[] inflated = inflate(decrypted); 108 | 109 | /* If inflate() returned the exact same array, that means the unzip failed, so we should assume 110 | that the compression shouldn't be used for this ITL file. */ 111 | boolean useCompression = !Arrays.equals(decrypted, inflated); 112 | 113 | return new Hdfm(version, unknown, headerRemainder, inflated, useCompression); 114 | } 115 | 116 | /** 117 | * hdfm chunks occur inline in 10.0, for some reason. 118 | * 119 | * @param di 120 | * @param length 121 | * @param consumed 122 | * @return 123 | * @throws IOException 124 | * @throws ItlException 125 | */ 126 | public static Hdfm readInline(Input di, int length, int consumed) throws IOException, ItlException 127 | { 128 | int hl = di.readInt(); 129 | 130 | if (hl != 0) { 131 | throw new IOException("Expected zero for inline HDFM length (was " + hl + ")"); 132 | } 133 | 134 | int fl = di.readInt(); 135 | 136 | int unknown = di.readInt(); 137 | 138 | int vsl = di.readUnsignedByte(); 139 | byte[] avs = new byte[vsl]; 140 | di.readFully(avs); 141 | 142 | String version = new String(avs, "us-ascii"); 143 | 144 | consumed += vsl + 13; 145 | 146 | byte[] headerRemainder = new byte[length - consumed]; 147 | di.readFully(headerRemainder); 148 | 149 | consumed += headerRemainder.length; 150 | 151 | if (consumed != length) { 152 | throw new IOException("Expected to read " + length + " bytes but read " + consumed); 153 | } 154 | 155 | return new Hdfm(version, unknown, headerRemainder, null, false); 156 | } 157 | 158 | /** 159 | * Obfuscation description from 160 | * this sample. 161 | * 162 | * @param orig 163 | * @param mode 164 | * @return 165 | * @throws UnsupportedEncodingException 166 | * @throws ItlException 167 | */ 168 | private static byte[] crypt(String version, byte[] orig, int mode) throws UnsupportedEncodingException, ItlException 169 | { 170 | byte[] res = new byte[orig.length]; 171 | 172 | /* Decrypt */ 173 | try { 174 | byte[] rawKey = "BHUILuilfghuila3".getBytes("us-ascii"); 175 | 176 | SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES"); 177 | Cipher cip = Cipher.getInstance("AES/ECB/NoPadding"); 178 | cip.init(mode, skeySpec); 179 | 180 | int encryptedLength = orig.length; 181 | 182 | if (ITunesVersion.isAtLeast(version, 10)) { 183 | encryptedLength = Math.min(encryptedLength, 102400); 184 | } 185 | 186 | encryptedLength -= encryptedLength % 16; 187 | 188 | int x = orig.length - encryptedLength; 189 | 190 | byte[] result = cip.doFinal(orig, 0, encryptedLength); 191 | System.arraycopy(result, 0, res, 0, result.length); 192 | System.arraycopy(orig, result.length, res, result.length, x); 193 | } catch (GeneralSecurityException gse) { 194 | if (mode == Cipher.DECRYPT_MODE) { 195 | throw new ItlException("Unable to decrypt library", gse); 196 | } else if (mode == Cipher.ENCRYPT_MODE) { 197 | throw new ItlException("Unable to encrypt library", gse); 198 | } else { 199 | throw new ItlException("Unable to perform operation", gse); 200 | } 201 | } 202 | 203 | return res; 204 | } 205 | 206 | static byte[] inflate(byte[] orig) throws ItlException, ZipException 207 | { 208 | /* Check for a zlib flag byte; 0x78 => 32k window, deflate */ 209 | boolean probablyCompressed = (orig.length >= 1 && orig[0] == 0x78); 210 | 211 | byte[] inflated = null; 212 | 213 | try 214 | { 215 | InflaterInputStream isInflater = new InflaterInputStream(new ByteArrayInputStream(orig), new Inflater()); 216 | ByteArrayOutputStream osDecompressed = new ByteArrayOutputStream(orig.length); 217 | inflated = new byte[orig.length]; 218 | int iDecompressed; 219 | while(true) 220 | { 221 | iDecompressed = isInflater.read(inflated, 0, orig.length); 222 | if (iDecompressed == -1) 223 | break; 224 | osDecompressed.write(inflated, 0, iDecompressed); 225 | } 226 | inflated = osDecompressed.toByteArray(); 227 | osDecompressed.close(); 228 | isInflater.close(); 229 | } 230 | catch (ZipException ze) 231 | { 232 | if (probablyCompressed) 233 | { 234 | throw ze; 235 | } 236 | // If a ZipException occurs, it's probably because "orig" isn't actually compressed data, 237 | // because it's from an earlier version of iTunes. 238 | // So since there's nothing to decompress, just return the array that was passed in, unchanged. 239 | return orig; 240 | } 241 | catch (IOException ioe) 242 | { 243 | throw new ItlException("Error when unzipping the file contents", ioe); 244 | } 245 | 246 | return inflated; 247 | } 248 | 249 | private static byte[] deflate(byte[] orig) throws ItlException 250 | { 251 | try 252 | { 253 | DeflaterInputStream isDeflater = new DeflaterInputStream(new ByteArrayInputStream(orig), new Deflater()); 254 | ByteArrayOutputStream osCompressed = new ByteArrayOutputStream(orig.length); 255 | byte[] deflated = new byte[orig.length]; 256 | int iCompressed; 257 | 258 | while(true) 259 | { 260 | iCompressed = isDeflater.read(deflated, 0, orig.length); 261 | if (iCompressed == -1) 262 | break; 263 | osCompressed.write(deflated, 0, iCompressed); 264 | } 265 | 266 | deflated = osCompressed.toByteArray(); 267 | osCompressed.close(); 268 | isDeflater.close(); 269 | 270 | return deflated; 271 | } 272 | catch (IOException ioe) 273 | { 274 | throw new ItlException("Error when zipping the file contents", ioe); 275 | } 276 | } 277 | 278 | public void write(DataOutput o) throws IllegalArgumentException, IOException, ItlException 279 | { 280 | write(o, fileData); 281 | } 282 | 283 | public void write(DataOutput o, byte[] dat) throws IllegalArgumentException, IOException, ItlException 284 | { 285 | if (this.compressContents) 286 | { 287 | /* If the contents were zipped before, we should zip them now, before encrypting and then writing to the file */ 288 | dat = deflate(dat); 289 | } 290 | 291 | /* Write the header */ 292 | byte[] ba = version.getBytes("us-ascii"); 293 | 294 | assert ba.length < 256; 295 | 296 | o.writeInt(Util.fromString("hdfm")); 297 | 298 | int hl = 17 + headerRemainder.length + ba.length; 299 | o.writeInt(hl); 300 | 301 | int fileLength = hl + dat.length; 302 | o.writeInt(fileLength); 303 | 304 | o.writeInt(unknown); 305 | 306 | 307 | o.writeByte(ba.length); 308 | o.write(ba); 309 | 310 | o.write(headerRemainder); 311 | 312 | /* Encode and write the data */ 313 | byte[] encrypted = crypt(version, dat, Cipher.ENCRYPT_MODE); 314 | 315 | o.write(encrypted); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /titl-core/src/main/java/org/kafsemo/titl/RandomizePlaylist.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2013 Lael Jones 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import java.io.ByteArrayInputStream; 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.DataOutputStream; 24 | import java.io.File; 25 | import java.io.FileInputStream; 26 | import java.io.FileOutputStream; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.io.UnsupportedEncodingException; 31 | import java.nio.ByteBuffer; 32 | import java.util.Collections; 33 | import java.util.List; 34 | import static org.kafsemo.titl.ParseLibrary.readGenericHohm; 35 | import static org.kafsemo.titl.ParseLibrary.readHdsm; 36 | import org.kafsemo.titl.diag.InputRange; 37 | 38 | /** 39 | * 40 | * @author Lael 41 | */ 42 | public class RandomizePlaylist { 43 | 44 | private PlaylistRawItems currentPlaylistItems; 45 | private String currentPlaylistTitle; 46 | 47 | public static void randomizePlaylist(File i, File o, String s) throws IOException, ItlException 48 | { 49 | long fileLength = i.length(); 50 | 51 | // System.out.println("Playlist : " + s); 52 | InputStream in = new FileInputStream(i); 53 | OutputStream out = new FileOutputStream(o); 54 | try { 55 | randomizePlaylist(in, fileLength, out, s); 56 | } finally { 57 | out.close(); 58 | in.close(); 59 | } 60 | } 61 | 62 | public static void randomizePlaylist(InputStream in, long fileLength, OutputStream out, String s) throws IOException, ItlException 63 | { 64 | Input di = new InputImpl(in); 65 | 66 | Hdfm hdr = Hdfm.read(di, fileLength); 67 | 68 | RandomizePlaylist pl = new RandomizePlaylist(); 69 | 70 | PlaylistRawItems playlist; 71 | playlist = pl.drainPlaylist(new InputImpl(new ByteArrayInputStream(hdr.fileData)), hdr.fileData.length, s); 72 | 73 | if (playlist == null) 74 | { 75 | System.out.printf("Unable to locate playlist %s\n", s); 76 | return; 77 | } 78 | 79 | // Get the item ids (song ids) from the playlist 80 | List pi = playlist.getItemIds(); 81 | Collections.shuffle(pi); 82 | 83 | // Starting byte offset within the data for the playlist items 84 | int offset = (int) playlist.getItemStartOffset(); 85 | int index = 0; 86 | 87 | List pbi = playlist.getRawItems(); 88 | 89 | // Byte array output stream that will contain the library file data 90 | // with the shuffled playlist 91 | ByteArrayOutputStream bo = new ByteArrayOutputStream(hdr.fileData.length); 92 | 93 | // Copy the library input to the output up to the place where the playlist 94 | // items start 95 | bo.write(hdr.fileData, 0, (int) playlist.getItemStartOffset()); 96 | 97 | // Loop through all raw playlist items 98 | for (ByteArrayOutputStream temp : pbi) 99 | { 100 | // Get the raw data in byte array form and convert the new song id 101 | // to a byte array suitable for writing out 102 | byte [] data = temp.toByteArray(); 103 | byte [] idArr = ByteBuffer.allocate(4).putInt(pi.get(index).intValue()).array(); 104 | 105 | // Write the new playlist item to a byte array 106 | ByteArrayOutputStream renum = new ByteArrayOutputStream(data.length); 107 | renum.write(data, 0, 24); 108 | renum.write(idArr, 0, 4); 109 | renum.write(data, 28, data.length - 28); 110 | 111 | // Write the new playlist item to the library output 112 | bo.write(renum.toByteArray()); 113 | 114 | offset += data.length; 115 | index++; 116 | } 117 | 118 | // Write the remainder of the library 119 | bo.write(hdr.fileData, offset, hdr.fileData.length - offset); 120 | 121 | // Write the new library 122 | DataOutputStream dos = new DataOutputStream(out); 123 | byte [] outputData = bo.toByteArray(); 124 | hdr.write(dos, outputData); 125 | } 126 | 127 | static void printData(byte [] data) 128 | { 129 | int idx = 0; 130 | while (idx < data.length) 131 | { 132 | if ((idx % 16) == 0) 133 | { 134 | System.out.printf("\n\t0x%04x : ", idx); 135 | } 136 | System.out.printf("0x%02x ", data[idx]); 137 | idx++; 138 | } 139 | System.out.printf("\n\n"); 140 | } 141 | 142 | PlaylistRawItems drainPlaylist(Input di, int totalLength, String playlistTitle) throws UnsupportedEncodingException, IOException, ItlException 143 | { 144 | int remaining = totalLength; 145 | 146 | boolean going = true; 147 | boolean foundPlaylist = false; 148 | boolean firstItemFound = false; 149 | 150 | currentPlaylistTitle = null; 151 | 152 | while(going) 153 | { 154 | InputRange thisChunk = new InputRange(di.getPosition()); 155 | 156 | int consumed = 0; 157 | String type = Util.toString(di.readInt()); 158 | consumed += 4; 159 | 160 | int length = di.readInt(); 161 | consumed += 4; 162 | // System.out.println(di.getPosition() + ": " + type + ": " + length); 163 | 164 | thisChunk.length = length; 165 | thisChunk.type = type; 166 | 167 | int recLength; 168 | 169 | if(type.equals("hohm")) 170 | { 171 | recLength = di.readInt(); 172 | consumed += 4; 173 | 174 | // System.out.println("HOHM length: " + recLength); 175 | 176 | int hohmType = di.readInt(); 177 | consumed += 4; 178 | 179 | // System.out.printf("hohm type: 0x%02x\n", hohmType); 180 | 181 | thisChunk.more = hohmType; 182 | switch (hohmType) 183 | { 184 | case 1: 185 | throw new IOException("Looks complicated..."); 186 | 187 | case 0x64: // (Smart?) Playlist title 188 | String title = readGenericHohm(di); 189 | // if (!title.equals("####!####")) { 190 | if (currentPlaylistItems != null) { 191 | if (currentPlaylistTitle != null) { 192 | throw new ItlException("Playlist title defined twice"); 193 | } 194 | if (firstItemFound && 195 | ((currentPlaylistItems.getExpectedItemCount() != currentPlaylistItems.getItemIds().size()) || 196 | (!currentPlaylistItems.getItemIds().isEmpty()))) 197 | { 198 | throw new ItlException("Playlist items not continuous"); 199 | } 200 | currentPlaylistTitle = title; 201 | if (title.equals(playlistTitle)) 202 | foundPlaylist = true; 203 | } else { 204 | throw new ItlException("Playlist title without defined playlist"); 205 | } 206 | // } 207 | // System.out.println("Playlist title: " + title + " " + foundPlaylist); 208 | consumed = recLength; 209 | break; 210 | 211 | // Types that can occur inside a playlist that we dont care about 212 | case 0x65: // Smart criteria 213 | case 0x66: // Smart info 214 | case 0x67: // Podcast info? 215 | case 0x69: 216 | case 0x6c: 217 | di.skipBytes(recLength - consumed); 218 | consumed = recLength; 219 | thisChunk.more = hohmType + " [ignored] "; 220 | if (currentPlaylistItems != null) 221 | { 222 | if (firstItemFound && 223 | ((currentPlaylistItems.getExpectedItemCount() != currentPlaylistItems.getItemIds().size()) || 224 | (!currentPlaylistItems.getItemIds().isEmpty()))) 225 | { 226 | throw new ItlException("Playlist items not continuous"); 227 | } 228 | } 229 | else { 230 | throw new ItlException("Playlist info without defined playlist"); 231 | } 232 | break; 233 | 234 | // no other hohm types should occur inside a playlist. If 235 | // they do then assume that the playlist has ended 236 | default: 237 | if (endCurrentPlaylist(foundPlaylist)) 238 | return currentPlaylistItems; 239 | di.skipBytes(recLength - consumed); 240 | consumed = recLength; 241 | thisChunk.more = hohmType + " [ignored] "; 242 | break; 243 | } 244 | } 245 | else if (type.equals("hpim")) 246 | { 247 | // Starting a new playlist ends the current playlist 248 | if (endCurrentPlaylist(foundPlaylist)) 249 | return currentPlaylistItems; 250 | readHpim(di, length); 251 | firstItemFound = false; 252 | consumed = length; 253 | } 254 | else if (type.equals("hptm")) 255 | { 256 | readHptm(di, length); 257 | if (!firstItemFound) 258 | { 259 | firstItemFound = true; 260 | currentPlaylistItems.itemStartOffset = thisChunk.origin; 261 | } 262 | consumed = length; 263 | } 264 | else if(type.equals("hdsm")) 265 | { 266 | // End the current playlist on hdsm as well 267 | if (endCurrentPlaylist(foundPlaylist)) 268 | return currentPlaylistItems; 269 | going = !readHdsm(di, length); 270 | consumed = length; 271 | } 272 | // Ignored types (not useful for playlist randomization) any of these 273 | // types also ends parsing of the current playlist 274 | else if (type.equals("hghm") || type.equals("halm") || type.equals("hilm") || type.equals("htlm") || type.equals("hplm") 275 | || type.equals("hiim") || type.equals("hqlm") || type.equals("hqim") || type.equals("htim") || type.equals("haim") 276 | || type.equals("hdfm")) 277 | { 278 | di.skipBytes(length - consumed); 279 | consumed = length; 280 | if (endCurrentPlaylist(foundPlaylist)) 281 | return currentPlaylistItems; 282 | } 283 | else 284 | { 285 | // hexDumpBytes(di, length - consumed); 286 | // consumed = length; 287 | if (Util.isIdentifier(type)) { 288 | throw new ItlException("Unhandled type: " + type); 289 | } else { 290 | throw new ItlException("Library format not understood; bad decryption (unhandled type: " 291 | + type + ")"); 292 | } 293 | } 294 | 295 | remaining -= consumed; 296 | } 297 | 298 | return null; 299 | } 300 | 301 | // Byte Length Comment 302 | // ----------------------- 303 | // 0 4 hpim 304 | // 4 4 N = length of data 305 | // 8 4 ? 306 | // 12 4 ? 307 | // 16 4 number of items (hptm) in playlist 308 | private void readHpim(Input di, int length) throws IOException, ItlException 309 | { 310 | int unknownA = di.readInt(); 311 | int unknownB = di.readInt(); 312 | 313 | int itemCount = di.readInt(); 314 | 315 | // System.out.println("HPIM items: " + itemCount); 316 | // System.out.printf("0x%04x%04x", unknownA, unknownB); 317 | // System.out.println(""); 318 | 319 | byte[] remaining = new byte[length - 20]; 320 | di.readFully(remaining); 321 | 322 | byte[] ppid = new byte[8]; 323 | System.arraycopy(remaining, 420, ppid, 0, ppid.length); 324 | 325 | currentPlaylistItems = new PlaylistRawItems(itemCount); 326 | } 327 | 328 | private void readHptm(Input di, int length) throws IOException, ItlException 329 | { 330 | // type/len byte arrays for recreating the entire hptm byte entries 331 | byte[] type = ByteBuffer.allocate(4).putInt(Util.fromString("hptm")).array(); 332 | byte[] len = ByteBuffer.allocate(4).putInt(length).array(); 333 | 334 | byte[] unknown1 = new byte[16]; 335 | di.readFully(unknown1); 336 | 337 | int key = di.readInt(); 338 | byte[] keyArr = ByteBuffer.allocate(4).putInt(key).array(); 339 | // System.out.println(" Key: " + key); 340 | 341 | if (currentPlaylistItems == null) { 342 | throw new ItlException("Playlist item outside playlist content"); 343 | } 344 | 345 | byte[] unknown2 = new byte[length - 28]; 346 | di.readFully(unknown2); 347 | 348 | // Reconstruct the entire hptm entry and store it in the playlist 349 | ByteArrayOutputStream bo = new ByteArrayOutputStream(length); 350 | bo.write(type, 0, type.length); 351 | bo.write(len, 0, len.length); 352 | bo.write(unknown1, 0, unknown1.length); 353 | bo.write(keyArr, 0, keyArr.length); 354 | bo.write(unknown2, 0, unknown2.length); 355 | 356 | currentPlaylistItems.addItem(key, bo); 357 | } 358 | 359 | boolean endCurrentPlaylist(boolean foundPlaylist) throws ItlException 360 | { 361 | // Ensure that when a playlist ends that the correct number of playlist 362 | // items were found and if the current playlist is not the desired 363 | // playlist null out the current playlist/title so it gets ignored 364 | if (currentPlaylistItems != null) 365 | { 366 | List items = currentPlaylistItems.getItemIds(); 367 | int expectedCount = currentPlaylistItems.getExpectedItemCount(); 368 | if (expectedCount != items.size()) 369 | { 370 | throw new ItlException("Expected " + expectedCount + " playlist items, got " + items.size()); 371 | } 372 | if (!foundPlaylist) 373 | { 374 | currentPlaylistItems = null; 375 | currentPlaylistTitle = null; 376 | } 377 | } 378 | 379 | return foundPlaylist; 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestHohmPodcast.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertNull; 23 | 24 | import java.io.ByteArrayInputStream; 25 | import java.io.IOException; 26 | 27 | import org.junit.Ignore; 28 | import org.junit.Test; 29 | 30 | public class TestHohmPodcast 31 | { 32 | private static final byte[] emptySample = { 33 | 0x03, 0x00, 0x00, 0x00, 34 | 0x00, 0x00, 0x00, 0x00, 35 | 0x00, 0x00, 0x00, 0x10, 36 | 0x70, 0x65, 0x63, 0x68, 37 | 0x00, 0x00, 0x00, 0x00, 38 | 0x73, 0x74, 0x72, 0x74 39 | }; 40 | 41 | private static final byte[] sample = { 42 | 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43 | 0x00, 0x00, 0x00, 0x10, 0x70, 0x65, 0x63, 0x68, 44 | 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x74, 45 | 0x00, 0x00, 0x02, 0x2b, 0x70, 0x65, 0x63, 0x68, 46 | 0x00, 0x00, 0x00, 0x00, 0x63, 0x68, 0x61, 0x63, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0f, 49 | 0x73, 0x65, 0x61, 0x6e, 0x00, 0x00, 0x00, 0x01, 50 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x01, (byte) 0xfb,0x63, 0x68, 0x61, 0x6e, 52 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 53 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 54 | 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, 0x01, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 56 | 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 57 | 0x77, 0x77, 0x2e, 0x74, 0x6d, 0x62, 0x67, 0x2e, 58 | 0x63, 0x6f, 0x6d, 0x2f, 0x5f, 0x6d, 0x65, 0x64, 59 | 0x69, 0x61, 0x2f, 0x5f, 0x70, 0x6f, 0x64, 0x2f, 60 | 0x70, 0x6f, 0x64, 0x63, 0x61, 0x73, 0x74, 0x2e, 61 | 0x78, 0x6d, 0x6c, 0x00, 0x00, 0x00, 0x3c, 0x68, 62 | 0x65, 0x64, 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 63 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 64 | 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, (byte) 0x9a, (byte) 0xd3, 65 | 0x16, 0x0b, (byte) 0xb5, 0x06, 0x59, 0x77, (byte) 0xa1, 0x00, 66 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 69 | 0x00, 0x00, 0x28, 0x6c, 0x69, 0x6e, 0x6b, 0x00, 70 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 72 | 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x74, 0x6d, 73 | 0x62, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, 74 | 0x00, 0x00, 0x44, 0x61, 0x75, 0x72, 0x6c, 0x00, 75 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 77 | 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x74, 0x6d, 78 | 0x62, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x5f, 79 | 0x6d, 0x65, 0x64, 0x69, 0x61, 0x2f, 0x5f, 0x70, 80 | 0x6f, 0x64, 0x2f, 0x54, 0x4d, 0x42, 0x47, 0x50, 81 | 0x4f, 0x44, 0x43, 0x41, 0x53, 0x54, 0x2e, 0x6a, 82 | 0x70, 0x67, 0x00, 0x00, 0x00, 0x00, 0x18, 0x69, 83 | 0x67, 0x6f, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xc5, 85 | 0x18, 0x4c, 0x5b, 0x00, 0x00, 0x00, 0x18, 0x69, 86 | 0x67, 0x6f, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x64, 89 | 0x61, 0x72, 0x6b, 0x00, 0x00, 0x00, 0x01, 0x00, 90 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 91 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x66, 92 | 0x75, 0x64, 0x67, 0x00, 0x00, 0x00, 0x01, 0x00, 93 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xa0, 0x73, 95 | 0x65, 0x74, 0x74, 0x00, 0x00, 0x00, 0x01, 0x00, 96 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 97 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 98 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 101 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 102 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 104 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 105 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 106 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 107 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 108 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 110 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0x00, 0x00, 0x00 115 | }; 116 | 117 | /* A real sample with a number of podcasts in */ 118 | public static final byte[] fullSample = { 119 | 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 120 | 0x10, 0x00, 0x00, 0x00, 0x70, 0x65, 0x63, 0x68, 121 | 0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x72, 0x74, 122 | 0x00, 0x00, 0x01, 0x66, 0x70, 0x65, 0x63, 0x68, 123 | 0x00, 0x00, 0x00, 0x00, 0x63, 0x68, 0x61, 0x63, 124 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 125 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x4a, 126 | 0x73, 0x65, 0x61, 0x6e, 0x00, 0x00, 0x00, 0x01, 127 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 128 | 0x00, 0x00, 0x01, 0x36, 0x63, 0x68, 0x61, 0x6e, 129 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 130 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 131 | 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, 0x01, 132 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 133 | 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 134 | 0x77, 0x77, 0x2e, 0x67, 0x75, 0x61, 0x72, 0x64, 135 | 0x69, 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x2e, 0x75, 136 | 0x6b, 0x2f, 0x70, 0x6f, 0x64, 0x63, 0x61, 0x73, 137 | 0x74, 0x73, 0x2f, 0x72, 0x69, 0x63, 0x6b, 0x79, 138 | 0x67, 0x65, 0x72, 0x76, 0x61, 0x69, 0x73, 0x2f, 139 | 0x6d, 0x70, 0x33, 0x2e, 0x78, 0x6d, 0x6c, 0x00, 140 | 0x00, 0x00, 0x3c, 0x68, 0x65, 0x64, 0x72, 0x00, 141 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 142 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 143 | 0x01, 0x54, 0x40, (byte) 0x92, (byte) 0xf4, (byte) 0x86, (byte) 0x87, 0x17, 144 | 0x1b, (byte) 0xcb, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 145 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 146 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 147 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x6c, 148 | 0x69, 0x6e, 0x6b, 0x00, 0x00, 0x00, 0x01, 0x00, 149 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 150 | 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 151 | 0x77, 0x2e, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 152 | 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x2e, 0x75, 0x6b, 153 | 0x2f, 0x72, 0x69, 0x63, 0x6b, 0x79, 0x67, 0x65, 154 | 0x72, 0x76, 0x61, 0x69, 0x73, 0x00, 0x00, 0x00, 155 | 0x00, 0x18, 0x69, 0x67, 0x6f, 0x64, 0x00, 0x00, 156 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 157 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 158 | 0x00, 0x18, 0x69, 0x67, 0x6f, 0x63, 0x00, 0x00, 159 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 160 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 161 | 0x00, 0x18, 0x64, 0x61, 0x72, 0x6b, 0x00, 0x00, 162 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 163 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 164 | 0x00, 0x18, 0x66, 0x75, 0x64, 0x67, 0x00, 0x00, 165 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 166 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 167 | 0x01, (byte) 0x8b, 0x70, 0x65, 0x63, 0x68, 0x00, 0x00, 168 | 0x00, 0x00, 0x63, 0x68, 0x61, 0x63, 0x00, 0x00, 169 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 170 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x6f, 0x73, 0x65, 171 | 0x61, 0x6e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 172 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 173 | 0x01, 0x5b, 0x63, 0x68, 0x61, 0x6e, 0x00, 0x00, 174 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 175 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x75, 0x72, 176 | 0x6c, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 177 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 178 | 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 179 | 0x2e, 0x74, 0x6d, 0x62, 0x67, 0x2e, 0x63, 0x6f, 180 | 0x6d, 0x2f, 0x5f, 0x6d, 0x65, 0x64, 0x69, 0x61, 181 | 0x2f, 0x5f, 0x70, 0x6f, 0x64, 0x2f, 0x70, 0x6f, 182 | 0x64, 0x63, 0x61, 0x73, 0x74, 0x2e, 0x78, 0x6d, 183 | 0x6c, 0x00, 0x00, 0x00, 0x3c, 0x68, 0x65, 0x64, 184 | 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 185 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 186 | 0x02, 0x00, 0x00, (byte) 0xe1, 0x72, (byte) 0xd6, (byte) 0xd9, 0x0d, 187 | 0x7e, (byte) 0xd4, 0x70, (byte) 0x99, 0x0f, 0x00, 0x00, 0x00, 188 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 189 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 190 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 191 | 0x28, 0x6c, 0x69, 0x6e, 0x6b, 0x00, 0x00, 0x00, 192 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 193 | 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 194 | 0x77, 0x77, 0x77, 0x2e, 0x74, 0x6d, 0x62, 0x67, 195 | 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 196 | 0x44, 0x61, 0x75, 0x72, 0x6c, 0x00, 0x00, 0x00, 197 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 198 | 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 199 | 0x77, 0x77, 0x77, 0x2e, 0x74, 0x6d, 0x62, 0x67, 200 | 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x5f, 0x6d, 0x65, 201 | 0x64, 0x69, 0x61, 0x2f, 0x5f, 0x70, 0x6f, 0x64, 202 | 0x2f, 0x54, 0x4d, 0x42, 0x47, 0x50, 0x4f, 0x44, 203 | 0x43, 0x41, 0x53, 0x54, 0x2e, 0x6a, 0x70, 0x67, 204 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 205 | 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 206 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 207 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 208 | 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 209 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 210 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x64, 0x61, 0x72, 211 | 0x6b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 212 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 213 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x66, 0x75, 0x64, 214 | 0x67, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 215 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 216 | 0x00, 0x00, 0x00, 0x01, 0x4c, 0x70, 0x65, 0x63, 217 | 0x68, 0x00, 0x00, 0x00, 0x00, 0x63, 0x68, 0x61, 218 | 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 219 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 220 | 0x30, 0x73, 0x65, 0x61, 0x6e, 0x00, 0x00, 0x00, 221 | 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 222 | 0x00, 0x00, 0x00, 0x01, 0x1c, 0x63, 0x68, 0x61, 223 | 0x6e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 224 | 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 225 | 0x3d, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, 226 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 227 | 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 228 | 0x39, 0x32, 0x33, 0x66, 0x72, 0x65, 0x65, 0x66, 229 | 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x61, 230 | 0x67, 0x65, 0x73, 0x2f, 0x70, 0x6f, 0x64, 0x63, 231 | 0x61, 0x73, 0x74, 0x2f, 0x38, 0x30, 0x2e, 0x72, 232 | 0x73, 0x73, 0x00, 0x00, 0x00, 0x3c, 0x68, 0x65, 233 | 0x64, 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 234 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 235 | 0x00, 0x03, 0x00, 0x01, (byte) 0x91, 0x11, (byte) 0xa6, (byte) 0xcf, 236 | 0x4f, (byte) 0xe8, (byte) 0xa3, (byte) 0xa6, (byte) 0xb3, 0x0d, 0x00, 0x00, 237 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 238 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 239 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 240 | 0x00, 0x2f, 0x6c, 0x69, 0x6e, 0x6b, 0x00, 0x00, 241 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 242 | 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 243 | 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x70, 0x65, 0x6e, 244 | 0x6e, 0x2e, 0x66, 0x72, 0x65, 0x65, 0x66, 0x6d, 245 | 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 246 | 0x18, 0x69, 0x67, 0x6f, 0x64, 0x00, 0x00, 0x00, 247 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 248 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 249 | 0x18, 0x69, 0x67, 0x6f, 0x63, 0x00, 0x00, 0x00, 250 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 251 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 252 | 0x18, 0x64, 0x61, 0x72, 0x6b, 0x00, 0x00, 0x00, 253 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 254 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 255 | 0x18, 0x66, 0x75, 0x64, 0x67, 0x00, 0x00, 0x00, 256 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 257 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 258 | 0x4c, 0x70, 0x65, 0x63, 0x68, 0x00, 0x00, 0x00, 259 | 0x00, 0x63, 0x68, 0x61, 0x63, 0x00, 0x00, 0x00, 260 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 261 | 0x00, 0x00, 0x00, 0x01, 0x30, 0x73, 0x65, 0x61, 262 | 0x6e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 263 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 264 | 0x1c, 0x63, 0x68, 0x61, 0x6e, 0x00, 0x00, 0x00, 265 | 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 266 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x75, 0x72, 0x6c, 267 | 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 268 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 269 | 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x65, 0x6e, 0x6e, 270 | 0x2e, 0x66, 0x72, 0x65, 0x65, 0x66, 0x6d, 0x2e, 271 | 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x61, 0x67, 0x65, 272 | 0x73, 0x2f, 0x70, 0x6f, 0x64, 0x63, 0x61, 0x73, 273 | 0x74, 0x2f, 0x34, 0x33, 0x31, 0x2e, 0x72, 0x73, 274 | 0x73, 0x00, 0x00, 0x00, 0x3c, 0x68, 0x65, 0x64, 275 | 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 276 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 277 | 0x03, 0x00, 0x00, (byte) 0xa8, (byte) 0xe5, (byte) 0xc8, 0x64, 0x70, 278 | 0x10, (byte) 0xae, (byte) 0xb1, (byte) 0xc0, 0x4b, 0x00, 0x00, 0x00, 279 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 280 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 281 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 282 | 0x2c, 0x6c, 0x69, 0x6e, 0x6b, 0x00, 0x00, 0x00, 283 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 284 | 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 285 | 0x70, 0x65, 0x6e, 0x6e, 0x2e, 0x66, 0x72, 0x65, 286 | 0x65, 0x66, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 287 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 288 | 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 289 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 290 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 291 | 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 292 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 293 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x64, 0x61, 0x72, 294 | 0x6b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 295 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 296 | 0x00, 0x00, 0x00, 0x00, 0x18, 0x66, 0x75, 0x64, 297 | 0x67, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 298 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 299 | 0x00, 0x00, 0x00, 0x01, (byte) 0x8f, 0x70, 0x65, 0x63, 300 | 0x68, 0x00, 0x00, 0x00, 0x00, 0x63, 0x68, 0x61, 301 | 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 302 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 303 | 0x73, 0x73, 0x65, 0x61, 0x6e, 0x00, 0x00, 0x00, 304 | 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 305 | 0x00, 0x00, 0x00, 0x01, 0x5f, 0x63, 0x68, 0x61, 306 | 0x6e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 307 | 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 308 | 0x35, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, 309 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 310 | 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 311 | 0x74, 0x69, 0x6b, 0x69, 0x62, 0x61, 0x72, 0x74, 312 | 0x76, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 313 | 0x65, 0x64, 0x73, 0x2f, 0x69, 0x74, 0x75, 0x6e, 314 | 0x65, 0x73, 0x00, 0x00, 0x00, 0x3c, 0x68, 0x65, 315 | 0x64, 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 316 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 317 | 0x00, 0x02, 0x00, 0x00, 0x47, 0x24, 0x41, 0x6b, 318 | 0x50, (byte) 0xc2, 0x0c, 0x57, (byte) 0xb4, 0x38, 0x00, 0x00, 319 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 320 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 321 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 322 | 0x00, 0x2d, 0x6c, 0x69, 0x6e, 0x6b, 0x00, 0x00, 323 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 324 | 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 325 | 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x74, 0x69, 0x6b, 326 | 0x69, 0x62, 0x61, 0x72, 0x74, 0x76, 0x2e, 0x63, 327 | 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x61, 328 | 0x75, 0x72, 0x6c, 0x00, 0x00, 0x00, 0x01, 0x00, 329 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 330 | 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6c, 0x69, 331 | 0x62, 0x73, 0x79, 0x6e, 0x2e, 0x63, 0x6f, 0x6d, 332 | 0x2f, 0x70, 0x6f, 0x64, 0x63, 0x61, 0x73, 0x74, 333 | 0x73, 0x2f, 0x74, 0x69, 0x6b, 0x69, 0x2f, 0x69, 334 | 0x6d, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x50, 0x6f, 335 | 0x64, 0x63, 0x61, 0x73, 0x74, 0x2d, 0x43, 0x6f, 336 | 0x76, 0x65, 0x72, 0x2e, 0x6a, 0x70, 0x67, 0x00, 337 | 0x00, 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 0x64, 338 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 339 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 340 | 0x00, 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 0x63, 341 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 342 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 343 | 0x00, 0x00, 0x00, 0x18, 0x64, 0x61, 0x72, 0x6b, 344 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 345 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 346 | 0x00, 0x00, 0x00, 0x18, 0x66, 0x75, 0x64, 0x67, 347 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 348 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 349 | 0x00, 0x00, 0x01, (byte) 0x8f, 0x70, 0x65, 0x63, 0x68, 350 | 0x00, 0x00, 0x00, 0x00, 0x63, 0x68, 0x61, 0x63, 351 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 352 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x73, 353 | 0x73, 0x65, 0x61, 0x6e, 0x00, 0x00, 0x00, 0x01, 354 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 355 | 0x00, 0x00, 0x01, 0x5f, 0x63, 0x68, 0x61, 0x6e, 356 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 357 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 358 | 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, 0x01, 359 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 360 | 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 361 | 0x75, 0x73, 0x63, 0x69, 0x66, 0x65, 0x72, 0x2e, 362 | 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x73, 0x63, 363 | 0x61, 0x73, 0x74, 0x2f, 0x70, 0x75, 0x73, 0x63, 364 | 0x61, 0x73, 0x74, 0x2e, 0x78, 0x6d, 0x6c, 0x00, 365 | 0x00, 0x00, 0x3c, 0x68, 0x65, 0x64, 0x72, 0x00, 366 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 367 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 368 | 0x00, 0x67, (byte) 0xef, 0x3a, (byte) 0xda, (byte) 0xa3, 0x68, (byte) 0x9d, 369 | 0x13, 0x0d, (byte) 0xa1, 0x00, 0x00, 0x00, 0x00, 0x00, 370 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 371 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 372 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f, 0x6c, 373 | 0x69, 0x6e, 0x6b, 0x00, 0x00, 0x00, 0x01, 0x00, 374 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 375 | 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x73, 376 | 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x75, 0x73, 377 | 0x63, 0x69, 0x66, 0x65, 0x72, 0x2e, 0x63, 0x6f, 378 | 0x6d, 0x00, 0x00, 0x00, 0x00, 0x45, 0x61, 0x75, 379 | 0x72, 0x6c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 380 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 381 | 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x70, 0x75, 0x73, 382 | 0x63, 0x69, 0x66, 0x65, 0x72, 0x2e, 0x63, 0x6f, 383 | 0x6d, 0x2f, 0x70, 0x75, 0x73, 0x63, 0x61, 0x73, 384 | 0x74, 0x2f, 0x70, 0x6f, 0x64, 0x69, 0x63, 0x6f, 385 | 0x6e, 0x5f, 0x70, 0x75, 0x73, 0x63, 0x69, 0x66, 386 | 0x65, 0x72, 0x2e, 0x6a, 0x70, 0x67, 0x00, 0x00, 387 | 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 0x64, 0x00, 388 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 389 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 390 | 0x00, 0x00, 0x18, 0x69, 0x67, 0x6f, 0x63, 0x00, 391 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 392 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 393 | 0x00, 0x00, 0x18, 0x64, 0x61, 0x72, 0x6b, 0x00, 394 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 395 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 396 | 0x00, 0x00, 0x18, 0x66, 0x75, 0x64, 0x67, 0x00, 397 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 398 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 399 | 0x00, 0x01, 0x6d, 0x70, 0x65, 0x63, 0x68, 0x00, 400 | 0x00, 0x00, 0x00, 0x63, 0x68, 0x61, 0x63, 0x00, 401 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 402 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x51, 0x73, 403 | 0x65, 0x61, 0x6e, 0x00, 0x00, 0x00, 0x01, 0x00, 404 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 405 | 0x00, 0x01, 0x3d, 0x63, 0x68, 0x61, 0x6e, 0x00, 406 | 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 407 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x75, 408 | 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 409 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 410 | 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 411 | 0x77, 0x2e, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 412 | 0x61, 0x6e, 0x2e, 0x63, 0x6f, 0x2e, 0x75, 0x6b, 413 | 0x2f, 0x70, 0x6f, 0x64, 0x63, 0x61, 0x73, 0x74, 414 | 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x65, 0x64, 0x79, 415 | 0x2f, 0x72, 0x69, 0x63, 0x6b, 0x79, 0x67, 0x65, 416 | 0x72, 0x76, 0x61, 0x69, 0x73, 0x2f, 0x6d, 0x34, 417 | 0x61, 0x2e, 0x78, 0x6d, 0x6c, 0x00, 0x00, 0x00, 418 | 0x3c, 0x68, 0x65, 0x64, 0x72, 0x00, 0x00, 0x00, 419 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 420 | 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x5e, 421 | 0x0f, (byte) 0xcb, 0x51, 0x07, (byte) 0xb4, 0x78, 0x6e, (byte) 0xa7, 422 | 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 423 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 424 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 425 | 0x00, 0x00, 0x00, 0x00, 0x3b, 0x6c, 0x69, 0x6e, 426 | 0x6b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 427 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 428 | 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 429 | 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 430 | 0x2e, 0x63, 0x6f, 0x2e, 0x75, 0x6b, 0x2f, 0x72, 431 | 0x69, 0x63, 0x6b, 0x79, 0x67, 0x65, 0x72, 0x76, 432 | 0x61, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 433 | 0x69, 0x67, 0x6f, 0x64, 0x00, 0x00, 0x00, 0x01, 434 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 435 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 436 | 0x69, 0x67, 0x6f, 0x63, 0x00, 0x00, 0x00, 0x01, 437 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 438 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 439 | 0x64, 0x61, 0x72, 0x6b, 0x00, 0x00, 0x00, 0x01, 440 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 441 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 442 | 0x66, 0x75, 0x64, 0x67, 0x00, 0x00, 0x00, 0x01, 443 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 444 | 0x00, 0x00, 0x00, 0x00 445 | }; 446 | 447 | @Test 448 | public void parseEmptySample() throws IOException 449 | { 450 | HohmPodcast hp = HohmPodcast.parse( 451 | new InputImpl(new ByteArrayInputStream(emptySample)), emptySample.length); 452 | 453 | assertNull(hp); 454 | } 455 | 456 | @Test 457 | public void parseSinglePodcast() throws IOException 458 | { 459 | HohmPodcast hp = HohmPodcast.parse( 460 | new InputImpl(new ByteArrayInputStream(sample)), 461 | sample.length); 462 | 463 | assertEquals("http://www.tmbg.com/_media/_pod/podcast.xml", hp.url); 464 | assertEquals("http://www.tmbg.com", hp.link); 465 | assertEquals("http://www.tmbg.com/_media/_pod/TMBGPODCAST.jpg", hp.aurl); 466 | } 467 | 468 | @Ignore("Doesn't work yet.") 469 | @Test 470 | public void parseMultiplePodcasts() throws IOException 471 | { 472 | HohmPodcast.dump( 473 | new InputImpl(new ByteArrayInputStream(fullSample)), 474 | sample.length); 475 | 476 | // HohmPodcast hp = HohmPodcast.parse( 477 | // new DataInputStream(new ByteArrayInputStream(fullSample)), 478 | // sample.length); 479 | // 480 | // System.out.println(hp.url); 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /titl-core/src/test/java/org/kafsemo/titl/TestParseLibrary.java: -------------------------------------------------------------------------------- 1 | /* 2 | * titl - Tools for iTunes Libraries 3 | * Copyright (C) 2008-2011 Joseph Walton 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Lesser General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package org.kafsemo.titl; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertFalse; 23 | import static org.junit.Assert.assertNotNull; 24 | import static org.junit.Assert.assertNull; 25 | import static org.junit.Assert.assertTrue; 26 | import static org.junit.Assert.fail; 27 | 28 | import java.io.File; 29 | import java.io.IOException; 30 | import java.util.ArrayList; 31 | import java.util.Arrays; 32 | import java.util.Collection; 33 | import java.util.Collections; 34 | import java.util.HashMap; 35 | import java.util.HashSet; 36 | import java.util.List; 37 | import java.util.Map; 38 | import java.util.Set; 39 | 40 | import org.junit.Test; 41 | 42 | public class TestParseLibrary 43 | { 44 | @Test 45 | public void testParseEmptyItunes80Library() throws IOException, ItlException 46 | { 47 | File f = new File("src/test/resources/Empty iTunes 8.0 Library.itl"); 48 | 49 | Library lib = ParseLibrary.parse(f); 50 | assertNotNull(lib); 51 | assertEquals("8.0", lib.getVersion()); 52 | assertEquals("file://localhost/C:/Users/Joseph/Music/iTunes/iTunes%20Music/", lib.getMusicFolder()); 53 | } 54 | 55 | @Test 56 | public void testParseEmptyItunes801Library() throws IOException, ItlException 57 | { 58 | File f = new File("src/test/resources/Empty iTunes 8.0.1 Library.itl"); 59 | 60 | Library lib = ParseLibrary.parse(f); 61 | assertNotNull(lib); 62 | assertEquals("8.0.1", lib.getVersion()); 63 | assertEquals("file://localhost/C:/Users/Joseph/Music/iTunes/iTunes%20Music/", lib.getMusicFolder()); 64 | 65 | assertEquals("301496C163C0DF40", Util.pidToString(lib.getLibraryPersistentId())); 66 | 67 | String[][] expectedPlaylists = { 68 | {"Music", "38", "301496C163C0DF4B"}, 69 | {"Movies", "83", "301496C163C0DF4C"}, 70 | {"TV Shows", "86", "301496C163C0DF4D"}, 71 | {"Podcasts", "89", "301496C163C0DF4A"}, 72 | {"Audiobooks", "80", "301496C163C0DF4E"}, 73 | {"Party Shuffle", "92", "301496C163C0DF48"}, 74 | {"Genius", "73", "301496C163C0DF51"}, 75 | {"90’s Music", "41", "301496C163C0DF42"}, 76 | {"Music Videos", "56", "301496C163C0DF47"}, 77 | {"My Top Rated", "44", "301496C163C0DF43"}, 78 | {"Recently Added", "53", "301496C163C0DF46"}, 79 | {"Recently Played", "50", "301496C163C0DF45"}, 80 | {"Top 25 Most Played", "47", "301496C163C0DF44"}, 81 | 82 | /* Expected, but not present in XML */ 83 | {"Rented Movies"}, 84 | {"Ringtones"}, 85 | 86 | /* In XML, but not a playlist in the binary */ 87 | // "Library", 88 | 89 | {"####!####", null, "301496C163C0DF41"} 90 | }; 91 | 92 | List expectedPlaylistNames = new ArrayList(expectedPlaylists.length); 93 | for (String[] pla : expectedPlaylists) { 94 | expectedPlaylistNames.add(pla[0]); 95 | } 96 | Collections.sort(expectedPlaylistNames); 97 | 98 | List playlistNames = new ArrayList(lib.getPlaylistNames()); 99 | Collections.sort(playlistNames); 100 | 101 | assertEquals(expectedPlaylistNames.toString(), playlistNames.toString()); 102 | 103 | /* Check IDs */ 104 | // Map expectedIds = new HashMap(); 105 | // for (String[] pla : expectedPlaylists) { 106 | // if(pla.length > 1) { 107 | // expectedIds.put(pla[0], Integer.parseInt(pla[1])); 108 | // } 109 | // } 110 | // 111 | // for (Playlist pl : lib.getPlaylists()) { 112 | // if(expectedIds.containsKey(pl.getTitle())) { 113 | // assertEquals(expectedIds.get(pl.getTitle()).intValue(), pl.getId()); 114 | // } 115 | // } 116 | 117 | /* Check PPIDs */ 118 | Map expectedPpids = new HashMap(); 119 | for (String[] pla : expectedPlaylists) { 120 | if(pla.length > 1) { 121 | expectedPpids.put(pla[0], pla[2]); 122 | } 123 | } 124 | 125 | for (Playlist pl : lib.getPlaylists()) { 126 | if(expectedPpids.containsKey(pl.getTitle())) { 127 | assertEquals("Checking PID for playlist: " + pl.getTitle(), 128 | expectedPpids.get(pl.getTitle()), Util.pidToString(pl.getPpid())); 129 | } 130 | } 131 | 132 | /* Check a single smart criteria */ 133 | Playlist pl90sMusic = null; 134 | for (Playlist pl : lib.getPlaylists()) { 135 | if (pl.getTitle().equals("90’s Music")) { 136 | pl90sMusic = pl; 137 | } 138 | } 139 | assertNotNull(pl90sMusic); 140 | 141 | byte[] expectedSmartInfo = Base64.decode( 142 | "AQEAAwAAAAIAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 143 | + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 144 | + "AAAAAA=="); 145 | 146 | assertEquals(Util.pidToString(expectedSmartInfo), Util.pidToString(pl90sMusic.smartInfo)); 147 | assertTrue(Arrays.equals(expectedSmartInfo, pl90sMusic.smartInfo)); 148 | 149 | byte[] expectedSmartCriteria = Base64.decode("U0xzdAABAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 150 | + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 151 | + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAEAAAAAAAAAAAAAAAAAAAAAAAAA" 152 | + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEAAAAAAAAB8YAAAAAAAAAAAAAAAAAAAAB" 153 | + "AAAAAAAAB88AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5AgAAAQAA" 154 | + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAB" 155 | + "AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA" 156 | + "AAAAAAAA"); 157 | 158 | assertEquals(Util.pidToString(expectedSmartCriteria), Util.pidToString(pl90sMusic.smartCriteria)); 159 | assertTrue(Arrays.equals(expectedSmartCriteria, pl90sMusic.smartCriteria)); 160 | } 161 | 162 | @Test 163 | public void testMinimalLibraryTracksLoaded() throws IOException, ItlException 164 | { 165 | File f = new File("src/test/resources/Minimal iTunes 8.0.1 Library.itl"); 166 | 167 | Library lib = ParseLibrary.parse(f); 168 | 169 | // Song 170 | Collection tracks = lib.getTracks(); 171 | assertNotNull(tracks); 172 | 173 | assertEquals("The library should contain all tracks from the podcast", 174 | 13, tracks.size()); 175 | } 176 | 177 | private static Track getTrack(Collection tracks, int trackId) 178 | { 179 | for (Track tt : tracks) { 180 | if (tt.getTrackId() == trackId) { 181 | return tt; 182 | } 183 | } 184 | 185 | fail("Track ID " + trackId + " not found"); 186 | return null; 187 | } 188 | 189 | private static Playlist getPlaylist(Collection pls, String name) 190 | { 191 | for (Playlist pl : pls) { 192 | if (pl.getTitle().equals(name)) { 193 | return pl; 194 | } 195 | } 196 | 197 | fail("Playlist " + name + " not found"); 198 | return null; 199 | } 200 | 201 | @Test 202 | public void testMinimalLibraryLocalTrack() throws IOException, ItlException 203 | { 204 | File f = new File("src/test/resources/Minimal iTunes 8.0.1 Library.itl"); 205 | 206 | Library lib = ParseLibrary.parse(f); 207 | 208 | Track t = getTrack(lib.getTracks(), 246); 209 | 210 | assertNotNull("The track list should contain track ID 246", t); 211 | 212 | /* Tracks exported in iTunes XML */ 213 | assertEquals(246, t.getTrackId()); 214 | assertEquals("Fortune", t.getName()); 215 | assertEquals("Kristin Hersh", t.getArtist()); 216 | assertEquals("CASH Music: Kristin Hersh", t.getAlbum()); 217 | assertEquals("Podcast", t.getGenre()); 218 | assertEquals("MPEG audio file", t.getKind()); 219 | assertEquals(8611627, t.getSize()); 220 | assertEquals(256574, t.getTotalTime()); 221 | assertEquals(2008, t.getYear()); 222 | assertEquals("2008-10-06T19:26:16Z", Dates.toString(t.getDateModified())); 223 | assertEquals("2008-10-06T19:26:16Z", Dates.toString(t.getDateAdded())); 224 | assertEquals(256, t.getBitRate()); 225 | // assertEquals(44100, t.getSampleRate()); 226 | // assertEquals("2008-09-28T16:00:00Z", Dates.toString(t.getReleaseDate())); 227 | // assertEquals(1, t.getArtworkCount()); 228 | // assertEquals("6738F834EEB48618", Util.pidToString(t.getPersistentId())); 229 | // assertEquals("File", t.getTrackType()); 230 | // assertTrue(t.isPodcast()); 231 | // assertTrue(t.isUnplayed()); 232 | 233 | assertEquals("C:\\Users\\Joseph\\Music\\iTunes\\iTunes Music\\Podcasts\\CASH Music_ Kristin Hersh\\Fortune.mp3", 234 | t.getLocation()); 235 | // assertEquals(4, t.getFileFolderCount()); 236 | // assertEquals(1, t.getLibraryFolderCount()); 237 | 238 | /* Extra fields */ 239 | assertEquals(0, t.getRating()); 240 | assertEquals("http://s3.amazonaws.com/cash_users/kristinhersh/Speedbath/Fortune/Fortune_256.mp3", t.getUrl()); 241 | 242 | assertEquals("kristin hersh, fortune, cash music", t.getItunesKeywords()); 243 | assertEquals("http://kristinhersh.cashmusic.org", t.getItunesSubtitle()); 244 | // assertEquals("Kristin Hersh", t.getAuthor()); 245 | 246 | assertEquals("file://localhost/C:/Users/Joseph/Music/iTunes/iTunes%20Music/Podcasts/CASH%20Music_%20Kristin%20Hersh/Fortune.mp3", 247 | t.getLocalUrl()); 248 | } 249 | 250 | @Test 251 | public void testMinimalLibraryUndownloadedTrack() throws IOException, ItlException 252 | { 253 | File f = new File("src/test/resources/Minimal iTunes 8.0.1 Library.itl"); 254 | 255 | Library lib = ParseLibrary.parse(f); 256 | 257 | Track t = getTrack(lib.getTracks(), 170); 258 | 259 | assertNotNull("The track list should contain track called Mississippi Kite", t); 260 | 261 | assertEquals(170, t.getTrackId()); // Implementation details 262 | assertEquals("Mississippi Kite", t.getName()); 263 | assertEquals("Kristin Hersh", t.getArtist()); 264 | assertEquals("CASH Music: Kristin Hersh", t.getAlbum()); 265 | assertEquals("Podcast", t.getGenre()); 266 | assertNull(t.getKind()); 267 | assertEquals(9243257, t.getSize()); 268 | assertEquals(288000, t.getTotalTime()); 269 | assertEquals(0, t.getYear()); 270 | assertNull(t.getDateModified()); 271 | assertEquals("2008-10-06T19:23:46Z", Dates.toString(t.getDateAdded())); 272 | assertEquals(0, t.getBitRate()); 273 | // assertEquals("2008-08-30T16:00:00Z", Dates.toString(t.getReleaseDate())); 274 | // assertEquals("File", t.getTrackType()); 275 | // assertTrue(t.isPodcast()); 276 | // assertTrue(t.isUnplayed()); 277 | 278 | assertNull(t.getLocation()); 279 | // assertEquals(4, t.getFileFolderCount()); 280 | // assertEquals(1, t.getLibraryFolderCount()); 281 | 282 | /* Extra fields */ 283 | assertEquals(0, t.getRating()); 284 | assertEquals("http://s3.amazonaws.com/cash_users/kristinhersh/Speedbath/MississippiKite/MississippiKite_256.mp3", t.getUrl()); 285 | 286 | assertEquals("kristin hersh, mississippi kite, cash music", t.getItunesKeywords()); 287 | assertEquals("http://kristinhersh.cashmusic.org", t.getItunesSubtitle()); 288 | // assertEquals("Kristin Hersh", t.getAuthor()); 289 | 290 | assertEquals("http://s3.amazonaws.com/cash_users/kristinhersh/Speedbath/MississippiKite/MississippiKite_256.mp3", 291 | t.getLocalUrl()); 292 | } 293 | 294 | @Test 295 | public void testMinimalLibraryPodcast() throws Exception 296 | { 297 | File f = new File("src/test/resources/Minimal iTunes 8.0.1 Library.itl"); 298 | 299 | Library lib = ParseLibrary.parse(f); 300 | 301 | // Podcast 302 | Collection podcasts = lib.getPodcasts(); 303 | assertNotNull(podcasts); 304 | assertEquals(1, podcasts.size()); 305 | 306 | Podcast p = podcasts.iterator().next(); 307 | assertEquals("http://feeds.cashmusic.org/kristinhersh/", p.getPodcastLocation()); 308 | assertEquals("CASH Music: Kristin Hersh", p.getPodcastTitle()); 309 | 310 | Set expectedAuthors = new HashSet(Arrays.asList("Kristin Hersh", "Xiu Xiu + Kristin Hersh")); 311 | 312 | assertEquals(expectedAuthors, new HashSet(p.getPodcastAuthors())); 313 | 314 | HohmPodcast hp = getPlaylist(lib.getPlaylists(), "Podcasts").getHohmPodcast(); 315 | 316 | assertEquals("http://kristinhersh.cashmusic.org", hp.link); 317 | assertEquals("http://feeds.cashmusic.org/kristinhersh/kristinhersh.jpg", hp.aurl); 318 | assertEquals("http://feeds.cashmusic.org/kristinhersh/", hp.url); 319 | 320 | Track podcastTrack = getTrack(lib.getTracks(), 152); 321 | 322 | assertEquals("Kristin Hersh is releasing a new album serially, one track per month, via her project at CASH Music. This podcast delivers each month's track, along with any other music she releases through CASH.", 323 | podcastTrack.getItunesSummary()); 324 | 325 | assertEquals("The music from Kristin Hersh's CASH Music project", podcastTrack.getItunesSubtitle()); 326 | } 327 | 328 | @Test 329 | public void testMinimalLibraryPlaylist() throws Exception 330 | { 331 | File f = new File("src/test/resources/Minimal iTunes 8.0.1 Library.itl"); 332 | 333 | Library lib = ParseLibrary.parse(f); 334 | 335 | // Playlist 336 | Collection playlists = lib.getPlaylists(); 337 | assertFalse(playlists.isEmpty()); 338 | 339 | Playlist library = null; 340 | 341 | for (Playlist pl : playlists) { 342 | if (pl.getTitle().equals("####!####")) { 343 | library = pl; 344 | } 345 | } 346 | 347 | assertNotNull("The Library playlist should be found", library); 348 | 349 | List items = library.getItems(); 350 | assertEquals(Collections.singletonList(246), items); 351 | } 352 | 353 | @Test 354 | public void testMinimalLibraryTracksLoaded2() throws IOException, ItlException 355 | { 356 | File f = new File("src/test/resources/iTunes 8.0.1 Library TMBG.itl"); 357 | 358 | Library lib = ParseLibrary.parse(f); 359 | 360 | // Song 361 | Collection tracks = lib.getTracks(); 362 | assertNotNull(tracks); 363 | 364 | assertEquals("The library should contain all tracks from the podcast", 365 | 4, tracks.size()); 366 | } 367 | 368 | @Test 369 | public void testMinimalLibraryLocalTrackTMBG() throws IOException, ItlException 370 | { 371 | File f = new File("src/test/resources/iTunes 8.0.1 Library TMBG.itl"); 372 | 373 | Library lib = ParseLibrary.parse(f); 374 | 375 | Track t = getTrack(lib.getTracks(), 194); 376 | 377 | assertNotNull("The track list should contain track ID 194", t); 378 | 379 | /* Fields exported in iTunes XML */ 380 | assertEquals(194, t.getTrackId()); 381 | assertEquals("TMBG Podcast 36B", t.getName()); 382 | assertEquals("They Might Be Giants", t.getArtist()); 383 | assertEquals("They Might Be Giants Podcast", t.getAlbum()); 384 | assertEquals("Podcast", t.getGenre()); 385 | assertEquals("MPEG audio file", t.getKind()); 386 | assertEquals(35062766, t.getSize()); 387 | assertEquals(2114351, t.getTotalTime()); 388 | assertEquals(2008, t.getYear()); 389 | assertEquals("2008-10-12T23:52:43Z", Dates.toString(t.getDateModified())); 390 | assertEquals("2008-10-12T23:52:42Z", Dates.toString(t.getDateAdded())); 391 | assertEquals(128, t.getBitRate()); 392 | // assertEquals(44100, t.getSampleRate()); 393 | // assertEquals("2008-09-24T17:22:42Z", Dates.toString(t.getReleaseDate())); 394 | // assertEquals(1, t.getArtworkCount()); 395 | // assertEquals("D3160BB5065977B1", Util.pidToString(t.getPersistentId())); 396 | // assertEquals("File", t.getTrackType()); 397 | // assertTrue(t.isPodcast()); 398 | // assertTrue(t.isUnplayed()); 399 | 400 | assertEquals("C:\\Users\\Joseph\\Music\\iTunes\\iTunes Music\\Podcasts\\They Might Be Giants Podcast\\TMBG Podcast 36B.mp3", 401 | t.getLocation()); 402 | // assertEquals(4, t.getFileFolderCount()); 403 | // assertEquals(1, t.getLibraryFolderCount()); 404 | 405 | /* Extra fields */ 406 | assertEquals(0, t.getRating()); 407 | assertEquals("http://www.tmbg.com/_media/_pod/TMBGPodcast36B.mp3", t.getUrl()); 408 | 409 | assertEquals("They Might Be Giants, TMBG, John Flansburgh, John Linnell, Birdhouse in Your Soul, Istanbul, Homestar ", t.getItunesKeywords()); 410 | assertEquals("Hosted by public radio's Duke of Dead Air-Cecil Portesque-broadcasting from a secret, very futuristic location. Includes exclusive They Might Be Giants recordings, previews and rarities.", t.getItunesSubtitle()); 411 | // assertEquals("Kristin Hersh", t.getAuthor()); 412 | 413 | assertEquals("file://localhost/C:/Users/Joseph/Music/iTunes/iTunes%20Music/Podcasts/They%20Might%20Be%20Giants%20Podcast/TMBG%20Podcast%2036B.mp3", 414 | t.getLocalUrl()); 415 | } 416 | 417 | @Test 418 | public void testMinimalLibraryPodcast2() throws Exception 419 | { 420 | File f = new File("src/test/resources/iTunes 8.0.1 Library TMBG.itl"); 421 | 422 | Library lib = ParseLibrary.parse(f); 423 | 424 | // Podcast 425 | Collection podcasts = lib.getPodcasts(); 426 | assertNotNull(podcasts); 427 | assertEquals(1, podcasts.size()); 428 | 429 | Podcast p = podcasts.iterator().next(); 430 | assertEquals("http://www.tmbg.com/_media/_pod/podcast.xml", p.getPodcastLocation()); 431 | assertEquals("They Might Be Giants Podcast", p.getPodcastTitle()); 432 | 433 | Set expectedAuthors = new HashSet(Arrays.asList("They Might Be Giants")); 434 | assertEquals(expectedAuthors, new HashSet(p.getPodcastAuthors())); 435 | 436 | 437 | HohmPodcast hp = getPlaylist(lib.getPlaylists(), "Podcasts").getHohmPodcast(); 438 | 439 | assertEquals("http://www.tmbg.com", hp.link); 440 | assertEquals("http://www.tmbg.com/_media/_pod/TMBGPODCAST.jpg", hp.aurl); 441 | assertEquals("http://www.tmbg.com/_media/_pod/podcast.xml", hp.url); 442 | 443 | Track podcastTrack = getTrack(lib.getTracks(), 156); 444 | 445 | assertEquals("Direct from Brooklyn's own They Might Be Giants, this is the only authorized and official podcast from the band. Often imitated, never remunerated. Specializing in original, live and/or rare material from the band, please enjoy TMBG's finest podcast!", 446 | podcastTrack.getItunesSummary()); 447 | 448 | assertEquals("The official podcast of They Might Be Giants.", podcastTrack.getItunesSubtitle()); 449 | 450 | assertEquals("http://www.tmbg.com/_media/_pod/podcast.xml", podcastTrack.getLocalUrl()); 451 | } 452 | 453 | /** 454 | * Libraries in this release are compressed. This test confirms that decompression 455 | * works. 456 | * 457 | * @throws IOException 458 | * @throws ItlException 459 | */ 460 | @Test 461 | public void testParseEmptyItunes9Library() throws IOException, ItlException 462 | { 463 | File f = new File("src/test/resources/Empty iTunes 9.0.3 Library.itl"); 464 | 465 | Library lib = ParseLibrary.parse(f); 466 | assertNotNull(lib); 467 | assertEquals("9.0.3", lib.getVersion()); 468 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 469 | } 470 | 471 | /** 472 | * Another test for a new release. 473 | * 474 | * @throws IOException 475 | * @throws ItlException 476 | */ 477 | @Test 478 | public void testParseEmptyItunes9_2Library() throws IOException, ItlException 479 | { 480 | File f = new File("src/test/resources/Empty iTunes 9.2.0 Library.itl"); 481 | 482 | Library lib = ParseLibrary.parse(f); 483 | assertNotNull(lib); 484 | assertEquals("9.2", lib.getVersion()); 485 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 486 | } 487 | 488 | /** 489 | * A new release with a new obfuscation method. 490 | * 491 | * @throws IOException 492 | * @throws ItlException 493 | */ 494 | @Test 495 | public void testParseEmptyItunes10_0Library() throws IOException, ItlException 496 | { 497 | File f = new File("src/test/resources/Empty iTunes 10.0 Library.itl"); 498 | 499 | Library lib = ParseLibrary.parse(f); 500 | assertNotNull(lib); 501 | assertEquals("10.0", lib.getVersion()); 502 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 503 | } 504 | 505 | /** 506 | * A new point release. 507 | * 508 | * @throws IOException 509 | * @throws ItlException 510 | */ 511 | @Test 512 | public void testParseEmptyItunes10_0_1Library() throws IOException, ItlException 513 | { 514 | File f = new File("src/test/resources/Empty iTunes 10.0.1 Library.itl"); 515 | 516 | Library lib = ParseLibrary.parse(f); 517 | assertNotNull(lib); 518 | assertEquals("10.0.1", lib.getVersion()); 519 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 520 | } 521 | 522 | @Test 523 | public void testParseEmptyItunes10_1Library() throws IOException, ItlException 524 | { 525 | File f = new File("src/test/resources/Empty iTunes 10.1 Library.itl"); 526 | 527 | Library lib = ParseLibrary.parse(f); 528 | assertNotNull(lib); 529 | assertEquals("10.1", lib.getVersion()); 530 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 531 | assertEquals("44328F10E636D81E", Util.pidToString(lib.getLibraryPersistentId())); 532 | } 533 | 534 | @Test 535 | public void testParseEmptyItunes10_2Library() throws IOException, ItlException 536 | { 537 | File f = new File("src/test/resources/Empty iTunes 10.2 Library.itl"); 538 | 539 | Library lib = ParseLibrary.parse(f); 540 | assertNotNull(lib); 541 | assertEquals("10.2.2", lib.getVersion()); 542 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 543 | assertEquals("3A587ACD4CC64C31", Util.pidToString(lib.getLibraryPersistentId())); 544 | } 545 | 546 | @Test 547 | public void testParseItunes10_2LibraryWithSingleTrack() throws IOException, ItlException 548 | { 549 | File f = new File("src/test/resources/iTunes 10.2.2 Library with single track.itl"); 550 | 551 | Library lib = ParseLibrary.parse(f); 552 | assertNotNull(lib); 553 | assertEquals("10.2.2", lib.getVersion()); 554 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 555 | assertEquals("35F8CA737C067601", Util.pidToString(lib.getLibraryPersistentId())); 556 | 557 | Collection tracks = lib.getTracks(); 558 | assertEquals(1, tracks.size()); 559 | 560 | Track t = getTrack(tracks, 221); 561 | 562 | assertEquals("Here Sometimes", t.getName()); 563 | assertNull(t.getAlbumPersistentId()); 564 | 565 | assertEquals("C14C9C03E7DBB0E7", Util.pidToString(t.getPersistentId())); 566 | } 567 | 568 | @Test 569 | public void testParseItunes10_2LibraryWithTrackWithArtwork() throws IOException, ItlException 570 | { 571 | File f = new File("src/test/resources/iTunes 10.2.2 Library with single track with artwork.itl"); 572 | 573 | Library lib = ParseLibrary.parse(f); 574 | assertNotNull(lib); 575 | assertEquals("10.2.2", lib.getVersion()); 576 | assertEquals("file://localhost/C:/Documents%20and%20Settings/joe/My%20Documents/My%20Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 577 | assertEquals("35F8CA737C067601", Util.pidToString(lib.getLibraryPersistentId())); 578 | 579 | Collection tracks = lib.getTracks(); 580 | assertEquals(1, tracks.size()); 581 | 582 | // XXX Why is this 64 now, and still 221 in the XML? 583 | Track t = getTrack(tracks, 64); 584 | 585 | assertEquals("Here Sometimes", t.getName()); 586 | assertEquals("00D1246314F75A0C", Util.pidToString(t.getAlbumPersistentId())); 587 | assertEquals("C14C9C03E7DBB0E7", Util.pidToString(t.getPersistentId())); 588 | } 589 | 590 | @Test 591 | public void inputForReturnsAnInput() throws IllegalArgumentException, IOException 592 | { 593 | byte[] stream = { 594 | 'h', 'd', 's', 'm', 595 | 0, 0, 0, 1 596 | }; 597 | 598 | Input input = ParseLibrary.inputFor(stream); 599 | assertNotNull(input); 600 | assertEquals(Util.fromString("hdsm"), input.readInt()); 601 | assertEquals(0x00000001, input.readInt()); 602 | } 603 | 604 | @Test 605 | public void inputForReturnsAByteFlippedInputForAStreamStartingWithMsdh() throws IllegalArgumentException, IOException 606 | { 607 | byte[] stream = { 608 | 'm', 's', 'd', 'h', 609 | 1, 0, 0, 0 610 | }; 611 | 612 | Input input = ParseLibrary.inputFor(stream); 613 | assertNotNull(input); 614 | assertEquals(Util.fromString("hdsm"), input.readInt()); 615 | assertEquals(0x00000001, input.readInt()); 616 | } 617 | 618 | @Test 619 | public void parseEmptyLibraryFromOsX() throws IOException, ItlException 620 | { 621 | File f = new File("src/test/resources/Empty OS X iTunes 11.1.5 Library.itl"); 622 | 623 | Library lib = ParseLibrary.parse(f); 624 | assertNotNull(lib); 625 | assertEquals("11.1.5", lib.getVersion()); 626 | assertEquals("file://localhost/Users/joe/Music/iTunes/iTunes%20Media/", lib.getMusicFolder()); 627 | assertEquals("C4409DDF7C1D6E21", Util.pidToString(lib.getLibraryPersistentId())); 628 | } 629 | } 630 | --------------------------------------------------------------------------------