├── .travis.yml ├── TODO.md ├── .gitignore ├── README.md ├── src ├── main │ ├── config │ │ └── license-templates │ │ │ └── APACHE-2.txt │ ├── resources │ │ ├── songbird2itunes.bat │ │ └── logback.xml │ ├── java │ │ └── info │ │ │ └── schnatterer │ │ │ ├── java │ │ │ ├── lang │ │ │ │ ├── package-info.java │ │ │ │ ├── XLong.java │ │ │ │ └── SystemClock.java │ │ │ └── util │ │ │ │ ├── package-info.java │ │ │ │ ├── jar │ │ │ │ ├── package-info.java │ │ │ │ └── Jar.java │ │ │ │ └── Sets.java │ │ │ └── songbird2itunes │ │ │ ├── migration │ │ │ ├── package-info.java │ │ │ └── Songbird2itunesMigration.java │ │ │ ├── NoSplitter.java │ │ │ ├── Songbird2itunesCli.java │ │ │ └── Songbird2itunesApp.java │ └── assembly │ │ └── assembly.xml └── test │ └── java │ └── info │ └── schnatterer │ ├── songbird2itunes │ ├── migration │ │ └── Songbird2itunesMigrationTest.java │ ├── Songbird2itunesCliTest.java │ └── Songbird2itunesAppTest.java │ └── java │ └── util │ └── SetsTest.java ├── pom.xml └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - (Option for ignoring all warnings) 2 | - (add application specific exception that always returns stats) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | ## Eclipse 15 | *.pydevproject 16 | .project 17 | .metadata 18 | bin/** 19 | tmp/** 20 | tmp/**/* 21 | *.tmp 22 | *.bak 23 | *.swp 24 | *~.nib 25 | local.properties 26 | .classpath 27 | .settings/ 28 | .loadpath 29 | 30 | ## Maven 31 | /target 32 | 33 | ## Logs 34 | *.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # songbird2itunes 2 | 3 | [![Build Status](https://travis-ci.org/schnatterer/songbird2itunes.svg?branch=master)](https://travis-ci.org/schnatterer/songbird2itunes) 4 | [![License](https://img.shields.io/github/license/schnatterer/songbird2itunes.svg)](LICENSE) 5 | 6 | Migration tool to migrate a songbird database to itunes 7 | 8 | ## How to use 9 | See the [wiki](https://github.com/schnatterer/songbird2itunes/wiki) for details on how to use. 10 | 11 | ## History 12 | See [releases](https://github.com/schnatterer/songbird2itunes/releases) 13 | -------------------------------------------------------------------------------- /src/main/config/license-templates/APACHE-2.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) ${project.inceptionYear} ${owner} 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/main/resources/songbird2itunes.bat: -------------------------------------------------------------------------------- 1 | @REM 2 | @REM Copyright (C) 2015 Johannes Schnatterer 3 | @REM 4 | @REM Licensed under the Apache License, Version 2.0 (the "License"); 5 | @REM you may not use this file except in compliance with the License. 6 | @REM You may obtain a copy of the License at 7 | @REM 8 | @REM http://www.apache.org/licenses/LICENSE-2.0 9 | @REM 10 | @REM Unless required by applicable law or agreed to in writing, software 11 | @REM distributed under the License is distributed on an "AS IS" BASIS, 12 | @REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @REM See the License for the specific language governing permissions and 14 | @REM limitations under the License. 15 | @REM 16 | 17 | @echo off 18 | java -jar %~dp0/songbird2itunes.jar %* -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/lang/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Contains extensions of java.lang 18 | * 19 | * @author schnatterer 20 | */ 21 | package info.schnatterer.java.lang; -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/util/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Contains extensions of java.util 18 | * 19 | * @author schnatterer 20 | */ 21 | package info.schnatterer.java.util; -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/util/jar/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Contains extensions of java.util.jar 18 | * 19 | * @author schnatterer 20 | */ 21 | package info.schnatterer.java.util.jar; -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/songbird2itunes/migration/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Contains all migration specific logic. 18 | * @author schnatterer 19 | * 20 | */ 21 | package info.schnatterer.songbird2itunes.migration; -------------------------------------------------------------------------------- /src/test/java/info/schnatterer/songbird2itunes/migration/Songbird2itunesMigrationTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes.migration; 17 | 18 | import static org.junit.Assert.assertTrue; 19 | 20 | import org.junit.Test; 21 | 22 | public class Songbird2itunesMigrationTest { 23 | 24 | @Test 25 | public void test() { 26 | assertTrue(true); 27 | } 28 | 29 | // TODO long values from SB are null 30 | // TODO dates from SB are null 31 | // TODO URLs are malformed 32 | // TODO iTunes Exceptions 33 | // TODO setting system clock fails 34 | // TODO ... 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/songbird2itunes/NoSplitter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes; 17 | 18 | import java.util.LinkedList; 19 | import java.util.List; 20 | 21 | import com.beust.jcommander.converters.IParameterSplitter; 22 | 23 | /** 24 | * jcommander parameter splitter that does not split parameters. 25 | * 26 | * @author schnatterer 27 | * 28 | */ 29 | public class NoSplitter implements IParameterSplitter { 30 | /* 31 | * (non-Javadoc) 32 | * 33 | * @see com.beust.jcommander.converters.IParameterSplitter#split(java.lang.String) 34 | */ 35 | @Override 36 | public List split(final String value) { 37 | List result = new LinkedList(); 38 | result.add(value); 39 | return result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/util/jar/Jar.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.java.util.jar; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.util.jar.Attributes; 21 | import java.util.jar.Manifest; 22 | 23 | /** 24 | * Basic abstraction from the jar file the code is contained in. 25 | * 26 | * @author schnatterer 27 | * 28 | */ 29 | public class Jar { 30 | 31 | /** 32 | * Tries to read an attribute build from the manifest of the 33 | * jar. 34 | * 35 | * @return the value of the build attribute or 36 | * null if not found or not even in a jar file 37 | * 38 | * @throws IOException 39 | * if an I/O error has occurred reading the manifest 40 | */ 41 | public static String getBuildNumberFromManifest() throws IOException { 42 | InputStream manifestStream = Thread.currentThread() 43 | .getContextClassLoader() 44 | .getResourceAsStream("META-INF/MANIFEST.MF"); 45 | if (manifestStream != null) { 46 | Manifest manifest = new Manifest(manifestStream); 47 | Attributes attributes = manifest.getMainAttributes(); 48 | return attributes.getValue("build"); 49 | } 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | bin 20 | 21 | zip 22 | 23 | 24 | 25 | 26 | false 27 | lib 28 | false 29 | 30 | 31 | 32 | 33 | 34 | 35 | ${project.basedir} 36 | / 37 | 38 | README* 39 | LICENSE* 40 | NOTICE* 41 | 42 | 43 | 44 | 45 | ${project.build.directory} 46 | / 47 | 48 | *.jar 49 | 50 | 51 | *javadoc.jar 52 | *sources.jar 53 | 54 | 55 | 56 | 57 | ${project.basedir}/src/main/resources/ 58 | / 59 | 60 | *.bat 61 | *.sh 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/lang/XLong.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.java.lang; 17 | 18 | /** 19 | * Extended Long - wrapper for {@link java.lang.Long} that provides extended 20 | * features. 21 | * 22 | * @author schnatterer 23 | * 24 | */ 25 | public class XLong { 26 | 27 | private final java.lang.Long wrappedLong; 28 | 29 | /** 30 | * Creates a new extended long from a standard java boxed long. 31 | * 32 | * @param wrappedLong 33 | * the java long instance to wrap 34 | */ 35 | public XLong(java.lang.Long wrappedLong) { 36 | this.wrappedLong = wrappedLong; 37 | } 38 | 39 | /** 40 | * Safely casts a {@link XLong} to an {@link Integer}. If {@link XLong} 41 | * cannot fit into an {@link Integer} an exception is thrown. 42 | * 43 | * @return an instance of l as {@link Integer} or 44 | * null 45 | * @throws IllegalArgumentException 46 | * when l is less than {@link Integer#MIN_VALUE} or 47 | * greater than {@link Integer#MAX_VALUE} 48 | */ 49 | public Integer toInt() throws IllegalArgumentException { 50 | if (wrappedLong == null) { 51 | return null; 52 | } 53 | if (wrappedLong < Integer.MIN_VALUE || wrappedLong > Integer.MAX_VALUE) { 54 | throw new IllegalArgumentException(wrappedLong 55 | + " cannot be cast to int without changing its value."); 56 | } 57 | return Integer.valueOf(wrappedLong.intValue()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 23 | System.out 24 | 25 | INFO 26 | ACCEPT 27 | DENY 28 | 29 | 30 | 31 | %m%n%nopex 32 | 33 | 34 | 35 | 36 | 37 | System.err 38 | 39 | warn 40 | 41 | 42 | 43 | %level: %m%n%nopex 44 | 45 | 46 | 47 | 48 | 49 | songbird2itunes.log 50 | 51 | %d{ISO8601} %-5level %logger - %m%n 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/test/java/info/schnatterer/songbird2itunes/Songbird2itunesCliTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertFalse; 20 | import static org.junit.Assert.assertNull; 21 | 22 | import org.apache.tools.ant.types.Commandline; 23 | import org.junit.Test; 24 | 25 | import com.beust.jcommander.ParameterException; 26 | 27 | public class Songbird2itunesCliTest { 28 | 29 | private static final Integer DEFAULT_RETRIES = 50; 30 | 31 | /** Calls CLI with minimal required parameters. */ 32 | @Test 33 | public void minimal() { 34 | Songbird2itunesCli args = parseArgs("path"); 35 | assertEquals("Unexpected main parameter", "path", args.getPath()); 36 | // Assert defaults 37 | assertEquals("Unexpected default for parameter retries", 38 | DEFAULT_RETRIES, args.getRetries()); 39 | assertFalse("Unexpected default for parameter isDateAddedWorkaround", 40 | args.isDateAddedWorkaround()); 41 | } 42 | 43 | /** Calls CLI with multiple main parameters. */ 44 | @Test 45 | public void multipleMainParams() { 46 | Songbird2itunesCli args = parseArgs("path path2"); 47 | // Ignore path2 48 | assertEquals("Unexpected main parameter", "path", args.getPath()); 49 | // Assert defaults 50 | assertEquals("Unexpected default for parameter retries", 51 | DEFAULT_RETRIES, args.getRetries()); 52 | assertFalse("Unexpected default for parameter isDateAddedWorkaround", 53 | args.isDateAddedWorkaround()); 54 | } 55 | 56 | /** Calls CLI with --help parameter. */ 57 | @Test 58 | public void help() { 59 | assertNull("Calling with help parameter returned unexepcted result", 60 | parseArgs("--help")); 61 | } 62 | 63 | /** Calls CLI with no main parameter. */ 64 | @Test(expected = ParameterException.class) 65 | public void noMain() { 66 | parseArgs("-r " + DEFAULT_RETRIES); 67 | } 68 | 69 | /** 70 | * Convenience method that takes just the parameters passed to CLI splits 71 | * them command-line-style (to {@link String} array), hands them to 72 | * {@link Songbird2itunesCli#readParams(String[], String)} and returns the 73 | * result. 74 | * 75 | * @param args 76 | * the args to pass to CLI 77 | * 78 | * @return the {@link Songbird2itunesCli} object for validation 79 | */ 80 | private Songbird2itunesCli parseArgs(String args) { 81 | String[] translateCommandline = Commandline.translateCommandline(args); 82 | return Songbird2itunesCli.readParams(translateCommandline, "prog name"); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/util/Sets.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.java.util; 17 | 18 | import java.util.Collection; 19 | import java.util.HashSet; 20 | import java.util.function.Function; 21 | import java.util.stream.Stream; 22 | 23 | /** 24 | * Implementation of basic set (in a mathematical sense) operations. 25 | * 26 | * @author schnatterer 27 | * 28 | */ 29 | public class Sets { 30 | 31 | /** 32 | * Returns the intersection of two sets, as stream. The streams might have 33 | * different types. The result might contain duplicates. 34 | * 35 | * @param a 36 | * set a, of type T 37 | * @param b 38 | * set b, of other type S. Recommendation: 39 | * Use a {@link HashSet} here, as a lot of 40 | * {@link Collection#contains(Object)} is called 41 | * @param convertAtoB 42 | * converts elements of a to be comparable with type 43 | * b 44 | * @param 45 | * type of set a 46 | * @param 47 | * type of set b 48 | * @return a new instance that contains the intersection between 49 | * a and bb 50 | */ 51 | public static Stream intersection(Collection a, 52 | Collection b, Function convertAtoB) { 53 | return a.stream().filter( 54 | aMember -> b.contains(convertAtoB.apply(aMember))); 55 | } 56 | 57 | /** 58 | * Returns the relative complement of b in a ("a without b"). The streams 59 | * might have different types. The result might contain duplicates. 60 | * 61 | * @param a 62 | * set a, of type T 63 | * @param b 64 | * set b, of other type S. Recommendation: 65 | * Use a {@link HashSet} here, as a lot of 66 | * {@link Collection#contains(Object)} is called 67 | * @param convertAtoB 68 | * converts elements of a to be comparable with type 69 | * b 70 | * @param 71 | * type of set a (the one b is removed 72 | * from) 73 | * @param 74 | * type of set b (the one that is removed from 75 | * a ) 76 | * 77 | * @return a new instance that contains the elements of a that 78 | * are not in bb 79 | */ 80 | public static Stream relativeComplement(Collection a, 81 | Collection b, Function convertAtoB) { 82 | return a.stream().filter( 83 | aMember -> !b.contains(convertAtoB.apply(aMember))); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/info/schnatterer/java/util/SetsTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.java.util; 17 | 18 | import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertThat; 21 | 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.stream.Collectors; 25 | 26 | import org.junit.Test; 27 | 28 | public class SetsTest { 29 | 30 | public static final StringWrapper SW1 = new StringWrapper("1"); 31 | public static final StringWrapper SW2 = new StringWrapper("2"); 32 | public static final StringWrapper SW3 = new StringWrapper("3"); 33 | public static final StringWrapper SW4 = new StringWrapper("4"); 34 | public static final StringWrapper SW5 = new StringWrapper("5"); 35 | public static final StringWrapper SW6 = new StringWrapper("6"); 36 | public static final StringWrapper SW7 = new StringWrapper("7"); 37 | 38 | /** 39 | * Test for 40 | * {@link Sets#intersection(java.util.Collection, java.util.Collection, java.util.function.Function)} 41 | * . 42 | */ 43 | @Test 44 | public void testIntersectionSet() { 45 | List a = Arrays.asList(new StringWrapper[] { SW1, SW2, 46 | SW3, SW4, SW5 }); 47 | List b = Arrays 48 | .asList(new String[] { "3", "4", "5", "6", "7" }); 49 | List expected = Arrays.asList(new StringWrapper[] { SW3, 50 | SW4, SW5 }); 51 | 52 | List actual = Sets.intersection(a, b, 53 | aMember -> aMember.getWrapped()).collect(Collectors.toList()); 54 | assertEquals("intersection returned unexpected result", expected, 55 | actual); 56 | } 57 | 58 | /** 59 | * Test for 60 | * {@link Sets#relativeComplement(java.util.Collection, java.util.Collection, java.util.function.Function) 61 | * . 62 | */ 63 | @Test 64 | public void testRelativeComplementA() { 65 | List a = Arrays.asList(new StringWrapper[] { SW3, SW4, 66 | SW5, SW6, SW7 }); 67 | List b = Arrays 68 | .asList(new String[] { "1", "2", "3", "4", "5" }); 69 | 70 | List expected = Arrays.asList(new StringWrapper[] { SW6, 71 | SW7 }); 72 | 73 | List actual = Sets.relativeComplement(a, b, 74 | aMember -> aMember.getWrapped()).collect(Collectors.toList()); 75 | assertThat("relative complement of a in b returned unexpected result", 76 | actual, containsInAnyOrder(expected.toArray())); 77 | } 78 | 79 | /** 80 | * Simple class that does not equal a string, but can be easily converted. 81 | * 82 | * @author schnatterer 83 | * 84 | */ 85 | private static class StringWrapper { 86 | private String wrapped; 87 | 88 | public StringWrapper(String wrapped) { 89 | this.wrapped = wrapped; 90 | } 91 | 92 | public String getWrapped() { 93 | return wrapped; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return wrapped.toString(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/songbird2itunes/Songbird2itunesCli.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes; 17 | 18 | import java.util.LinkedList; 19 | import java.util.List; 20 | 21 | import com.beust.jcommander.JCommander; 22 | import com.beust.jcommander.Parameter; 23 | import com.beust.jcommander.ParameterException; 24 | 25 | /** 26 | * The Songbird2itunesCli command line interface takes care of parsing the 27 | * arguments and printing out potential errors 28 | * 29 | * @author schnatterer 30 | * 31 | */ 32 | public class Songbird2itunesCli { 33 | 34 | // Don't instantiate! 35 | private Songbird2itunesCli() { 36 | } 37 | 38 | /** 39 | * Using the {@link JCommander} framework to parse parameters. 40 | * 41 | * See http://jcommander.org/ 42 | */ 43 | private JCommander commander = null; 44 | 45 | private static final String EOL = System.getProperty("line.separator"); 46 | 47 | /** Description for main parameter - path to songbird database. */ 48 | private static final String DESC_DB = "Path to songbird database file"; 49 | 50 | private static final String DESC_RETR = "(optional) Number of retries after an iTunes error"; 51 | private static final String DESC_DATE_ADDED = "(optional) workaround for migrating the date added to iTunes. NOTE: This requires admin rights and set your system date before adding each track. Use with extreme care."; 52 | private static final String DESC_HELP = "(optional) Show this message"; 53 | private static final String DESC_PLAYLIST_NAMES = "(optional) Names of the playlists that should be migrated. If not specified, all playlist are migrated."; 54 | private static final String DESC_PLAYLISTS_ONLY = "(optional) Migrate only the playlists and the tracks within playlists. Don't migrate other tracks."; 55 | 56 | /** 57 | * Reads the command line parameters and prints error messages when 58 | * something went wrong 59 | * 60 | * @param argv 61 | * command line arguments to read 62 | * @param programName 63 | * program name displayed in the usage 64 | * 65 | * @return an instance of {@link Songbird2itunesCli} when everything went 66 | * ok, or null if "-- help" was called. 67 | * 68 | * @throws ParameterException 69 | * when something went wrong 70 | */ 71 | public static Songbird2itunesCli readParams(String[] argv, 72 | String programName) throws ParameterException { 73 | Songbird2itunesCli cliParams = new Songbird2itunesCli(); 74 | try { 75 | cliParams.commander = new JCommander(cliParams); 76 | cliParams.commander.setProgramName(programName); 77 | cliParams.commander.parse(argv); 78 | } catch (ParameterException e) { 79 | // Print err 80 | StringBuilder errStr = new StringBuilder(e.getMessage() + EOL); 81 | cliParams.commander.usage(errStr, " "); 82 | System.err.println(errStr.toString()); 83 | // Rethrow, so the main application knows something went wrong 84 | throw e; 85 | } 86 | 87 | if (cliParams.help == true) { 88 | cliParams.commander.usage(); 89 | cliParams = null; 90 | } 91 | 92 | return cliParams; 93 | } 94 | 95 | /** Definition of parameter - main parameter (path to songbird db). */ 96 | @Parameter(required = true, arity = 1, description = DESC_DB) 97 | private List mainParams; 98 | 99 | @Parameter(names = { "-r", "--retries" }, description = DESC_RETR) 100 | private final Integer retries = 50; 101 | 102 | @Parameter(names = { "-d", "--dateadded" }, description = DESC_DATE_ADDED) 103 | private boolean dateAddedWorkaround = false; 104 | 105 | @Parameter(names = { "-n", "--playlistnames" }, variableArity = true, splitter = NoSplitter.class, description = DESC_PLAYLIST_NAMES) 106 | private List playlistNames = new LinkedList<>(); 107 | 108 | @Parameter(names = { "-p", "--playlistsonly" }, description = DESC_PLAYLISTS_ONLY) 109 | private boolean playlistsOnly = false; 110 | 111 | @Parameter(names = "--help", help = true, description = DESC_HELP) 112 | private boolean help; 113 | 114 | /** 115 | * @return the main parameter - path to songbird database 116 | */ 117 | public String getPath() { 118 | return mainParams.get(0); 119 | } 120 | 121 | /** 122 | * @return the retries 123 | */ 124 | public Integer getRetries() { 125 | return retries; 126 | } 127 | 128 | /** 129 | * @return the dateAddedWorkaround 130 | */ 131 | public boolean isDateAddedWorkaround() { 132 | return dateAddedWorkaround; 133 | } 134 | 135 | /** 136 | * @return the playlistNames 137 | */ 138 | public List getPlaylistNames() { 139 | return playlistNames; 140 | } 141 | 142 | /** 143 | * @return the playlistsOnly 144 | */ 145 | public boolean isPlaylistsOnly() { 146 | return playlistsOnly; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/test/java/info/schnatterer/songbird2itunes/Songbird2itunesAppTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.mockito.Matchers.anyBoolean; 20 | import static org.mockito.Matchers.anyInt; 21 | import static org.mockito.Matchers.anyListOf; 22 | import static org.mockito.Matchers.anyString; 23 | import static org.mockito.Mockito.never; 24 | import static org.mockito.Mockito.verify; 25 | import static org.mockito.Mockito.when; 26 | import info.schnatterer.itunes4j.exception.ITunesException; 27 | import info.schnatterer.songbird2itunes.migration.Songbird2itunesMigration; 28 | import info.schnatterer.songbird2itunes.migration.Songbird2itunesMigration.Statistics; 29 | 30 | import java.io.ByteArrayInputStream; 31 | import java.io.InputStream; 32 | import java.sql.SQLException; 33 | import java.util.LinkedList; 34 | 35 | import org.apache.tools.ant.types.Commandline; 36 | import org.junit.Rule; 37 | import org.junit.Test; 38 | import org.mockito.Mock; 39 | import org.mockito.junit.MockitoJUnit; 40 | import org.mockito.junit.MockitoRule; 41 | 42 | public class Songbird2itunesAppTest { 43 | 44 | @Rule 45 | public MockitoRule rule = MockitoJUnit.rule(); 46 | 47 | @Mock 48 | private Songbird2itunesMigration s2iMock; 49 | 50 | private final Songbird2itunesApp classUnderTest = new Songbird2itunesApp4Test( 51 | "yes"); 52 | 53 | /** 54 | * Asserts the proper return code when calling with --help parameters. 55 | */ 56 | @Test 57 | public void help() { 58 | assertEquals("Running with help parameter returned unexpected result", 59 | Songbird2itunesApp.EXIT_SUCCESS, 60 | classUnderTest.run(new String[] { "--help" })); 61 | } 62 | 63 | /** 64 | * Asserts the proper return code when calling with all parameters and that 65 | * parameters are passed properly. 66 | * 67 | * @throws ITunesException 68 | * @throws SQLException 69 | */ 70 | @Test 71 | public void allParams() throws SQLException, ITunesException { 72 | when( 73 | s2iMock.migrate(anyString(), anyInt(), anyBoolean(), 74 | anyListOf(String.class), anyBoolean())).thenReturn( 75 | new Statistics()); 76 | assertEquals("Running with all parameters returned unexpected result", 77 | 0, classUnderTest.run(Commandline 78 | .translateCommandline("-r 23 -d path"))); 79 | verify(s2iMock).migrate("path", 23, Boolean.TRUE, 80 | new LinkedList(), false); 81 | } 82 | 83 | /** 84 | * Asserts proper return code when an exception is thrown during parameter 85 | * handling. 86 | * 87 | * @throws ITunesException 88 | * @throws SQLException 89 | */ 90 | @Test 91 | public void invalidParams() throws SQLException, ITunesException { 92 | assertEquals( 93 | "Running with invalid parameters returned unexpected result", 94 | Songbird2itunesApp.EXIT_INVALID_PARAMS, 95 | classUnderTest.run(new String[] {})); 96 | } 97 | 98 | /** 99 | * Asserts proper return code when an exception is thrown during conversion. 100 | * 101 | * @throws ITunesException 102 | * @throws SQLException 103 | */ 104 | @Test 105 | public void errorConversion() throws SQLException, ITunesException { 106 | when( 107 | s2iMock.migrate(anyString(), anyInt(), anyBoolean(), 108 | anyListOf(String.class), anyBoolean())).thenThrow( 109 | new RuntimeException("Mocked exception")); 110 | assertEquals("Error during conversoin returned unexpected result", 111 | Songbird2itunesApp.EXIT_ERROR_CONVERSION, 112 | classUnderTest.run(Commandline 113 | .translateCommandline("-r 23 -d path"))); 114 | } 115 | 116 | /** 117 | * Asserts proper return code and behavior when user does not confirm usage 118 | * of date added workaround. 119 | * 120 | * @throws ITunesException 121 | * @throws SQLException 122 | */ 123 | @Test 124 | public void notConfirmedWorkaround() throws SQLException, ITunesException { 125 | when( 126 | s2iMock.migrate(anyString(), anyInt(), anyBoolean(), 127 | anyListOf(String.class), anyBoolean())).thenReturn( 128 | new Statistics()); 129 | Songbird2itunesApp classUnderTestNoConfirmation = new Songbird2itunesApp4Test( 130 | "no confirm"); 131 | assertEquals( 132 | "Denying confirmation for using the workaround returned unexpected result", 133 | Songbird2itunesApp.EXIT_SUCCESS, classUnderTestNoConfirmation 134 | .run(Commandline.translateCommandline("-r 23 -d path"))); 135 | verify(s2iMock, never()).migrate(anyString(), anyInt(), anyBoolean(), 136 | anyListOf(String.class), anyBoolean()); 137 | } 138 | 139 | private class Songbird2itunesApp4Test extends Songbird2itunesApp { 140 | private String confirmationString; 141 | 142 | public Songbird2itunesApp4Test(String confirmationString) { 143 | this.confirmationString = confirmationString; 144 | } 145 | 146 | @Override 147 | Songbird2itunesMigration createSongbird2itunes() { 148 | return s2iMock; 149 | } 150 | 151 | @Override 152 | InputStream createSystemIn() { 153 | return new ByteArrayInputStream(new String(confirmationString 154 | + "\n").getBytes()); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/java/lang/SystemClock.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.java.lang; 17 | 18 | import java.io.IOException; 19 | import java.text.DateFormat; 20 | import java.text.SimpleDateFormat; 21 | import java.util.Date; 22 | 23 | /** 24 | * A java wrapper for the system clock. For now this will 25 | *
    26 | *
  • only work on windows
  • 27 | *
  • only work when run as administrator
  • 28 | *
29 | * So better don't use this :) 30 | * 31 | * @author schnatterer 32 | * 33 | */ 34 | public class SystemClock { 35 | /** 36 | * Thread-safe holder for the {@link DateFormat} used for formatting the 37 | * time values. 38 | */ 39 | private static ThreadLocal dateFormatHolderTime = new ThreadLocal() { 40 | @Override 41 | protected DateFormat initialValue() { 42 | DateFormat dateFormat = new SimpleDateFormat("HH:mm"); 43 | return dateFormat; 44 | } 45 | }; 46 | /** 47 | * Thread-safe holder for the {@link DateFormat} used for formatting the 48 | * date values. 49 | */ 50 | private static ThreadLocal dateFormatHolderDate = new ThreadLocal() { 51 | @Override 52 | protected DateFormat initialValue() { 53 | DateFormat dateFormat = new SimpleDateFormat("dd-MM-yy"); 54 | return dateFormat; 55 | } 56 | }; 57 | 58 | /** 59 | * Sets the system clock synchronously (that is, by forking a process and 60 | * waiting for its termination) without throwing exceptions. Any errors are 61 | * logged. 62 | * 63 | * @param date 64 | * the date and time to se 65 | * 66 | * @throws SystemClockException 67 | * wraps all exceptions 68 | */ 69 | public void set(Date date) throws SystemClockException { 70 | // Windows "cmd /C date dd-MM-yy & time hh:mm:ss" 71 | synchronousExec("cmd /C date " 72 | + dateFormatHolderDate.get().format(date) + " & time " 73 | + dateFormatHolderTime.get().format(date)); 74 | // Linux: "date -s MMddhhmm[[yy]yy]" 75 | } 76 | 77 | /** 78 | * Resyncs the system clock with a time server. 79 | * 80 | * @throws SystemClockException 81 | * wraps all exceptions 82 | */ 83 | public void resync() throws SystemClockException { 84 | synchronousExec("cmd /C w32tm /resync /force"); 85 | } 86 | 87 | /** 88 | * Higher level level wrapper for synchronous system calls. Forks a process 89 | * and waits for the result. Any errors are uniformly wrapped in a 90 | * {@link SystemClockException}. 91 | * 92 | * @param execCommand 93 | * command to fork and execute 94 | * 95 | * @throws SystemClockException 96 | * wraps all exceptions 97 | */ 98 | protected void synchronousExec(String execCommand) 99 | throws SystemClockException { 100 | try { 101 | Process proc = exec(execCommand); 102 | int exitValue = proc.waitFor(); 103 | if (exitValue != 0) { 104 | throw new SystemClockException( 105 | "Setting the system clock failed (\"" + execCommand 106 | + "\"): Exit value " + exitValue); 107 | } 108 | } catch (IOException | InterruptedException | RuntimeException e) { 109 | throw new SystemClockException( 110 | "Setting the system clock failed (\"" + execCommand 111 | + "\") with exception: " + e.getMessage(), e); 112 | } 113 | } 114 | 115 | /** 116 | * Low level wrapper for {@link Runtime#exec(String)}. Useful for testing. 117 | * 118 | * @param execCommand 119 | * a specified system command. 120 | * 121 | * @return A new {@link Process} object for managing the subprocess 122 | * 123 | * @throws IOException 124 | * If an I/O error occurs 125 | */ 126 | private Process exec(String execCommand) throws IOException { 127 | return Runtime.getRuntime().exec(execCommand); 128 | } 129 | 130 | /** 131 | * Wraps all kinds of system-specific exceptions that might occur when 132 | * setting the system clock. 133 | * 134 | * @author schnatterer 135 | */ 136 | public static class SystemClockException extends Exception { 137 | private static final long serialVersionUID = 1L; 138 | 139 | /** 140 | * Constructs a new exception with the specified detail message and 141 | * cause. 142 | *

143 | * Note that the detail message associated with {@code cause} is 144 | * not automatically incorporated in this exception's detail 145 | * message. 146 | * 147 | * @param message 148 | * the detail message (which is saved for later retrieval by 149 | * the {@link #getMessage()} method). 150 | * @param cause 151 | * the cause (which is saved for later retrieval by the 152 | * {@link #getCause()} method). (A null value is 153 | * permitted, and indicates that the cause is nonexistent or 154 | * unknown.) 155 | */ 156 | public SystemClockException(String message, Throwable cause) { 157 | super(message, cause); 158 | } 159 | 160 | /** 161 | * Constructs a new exception with the specified detail message. The 162 | * cause is not initialized, and may subsequently be initialized by a 163 | * call to {@link #initCause}. 164 | * 165 | * @param message 166 | * the detail message. The detail message is saved for later 167 | * retrieval by the {@link #getMessage()} method. 168 | */ 169 | public SystemClockException(String message) { 170 | super(message); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/songbird2itunes/Songbird2itunesApp.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes; 17 | 18 | import info.schnatterer.java.util.jar.Jar; 19 | import info.schnatterer.songbird2itunes.migration.Songbird2itunesMigration; 20 | import info.schnatterer.songbird2itunes.migration.Songbird2itunesMigration.Statistics; 21 | 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.util.Scanner; 25 | 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import com.beust.jcommander.ParameterException; 30 | 31 | /** 32 | * Entry point for the songbird2Itunes Command Line Application 33 | */ 34 | public class Songbird2itunesApp { 35 | static final int EXIT_SUCCESS = 0; 36 | static final int EXIT_INVALID_PARAMS = 1; 37 | static final int EXIT_ERROR_CONVERSION = 2; 38 | static final String PROG_NAME = "songbird2itunes"; 39 | 40 | /** SLF4J-Logger. */ 41 | private final Logger log = LoggerFactory.getLogger(getClass()); 42 | 43 | public static void main(String[] args) { 44 | System.exit(new Songbird2itunesApp().run(args)); 45 | } 46 | 47 | /** 48 | * @param args 49 | * @return 0 on success; 1 on command line parameters error; 2 on error on 50 | * songbird 2 iTunes conversion. 51 | */ 52 | int run(String[] args) { 53 | /* Parse command line arguments/parameter (command line interface) */ 54 | int ret = 0; // Presume success 55 | Songbird2itunesCli cliParams = null; 56 | 57 | printWelcomeMessage(); 58 | 59 | try { 60 | cliParams = Songbird2itunesCli.readParams(args, PROG_NAME); 61 | if (cliParams != null) { 62 | if (cliParams.isDateAddedWorkaround() && !confirmedWorkaround()) { 63 | return EXIT_SUCCESS; 64 | } 65 | // Successfully read command line params. Do conversion 66 | printStats(createSongbird2itunes().migrate(cliParams.getPath(), 67 | cliParams.getRetries(), 68 | cliParams.isDateAddedWorkaround(), 69 | cliParams.getPlaylistNames(), 70 | cliParams.isPlaylistsOnly())); 71 | return EXIT_SUCCESS; 72 | } 73 | } catch (ParameterException e) { 74 | log.error("Error parsing command line arguments."); 75 | ret = EXIT_INVALID_PARAMS; 76 | } catch (Exception e) { /* 77 | * Outmost "catch all" block for logging any 78 | * exception exiting application with error 79 | */ 80 | log.error("Conversion failed with error \"" + e.getMessage() 81 | + "\". Please see log file.", e); 82 | ret = EXIT_ERROR_CONVERSION; 83 | } 84 | return ret; 85 | } 86 | 87 | /** 88 | * Writes a welcome message to the log/console, including a build number, if 89 | * available. 90 | */ 91 | private void printWelcomeMessage() { 92 | String welcomeMessage = "Welcome to " + PROG_NAME; 93 | String buildNumber; 94 | try { 95 | buildNumber = Jar.getBuildNumberFromManifest(); 96 | if (buildNumber != null) { 97 | welcomeMessage = welcomeMessage + " (" + buildNumber + ")"; 98 | } 99 | } catch (IOException e) { 100 | // If something fails we just don't print the build number 101 | } 102 | log.info(welcomeMessage); 103 | } 104 | 105 | /** 106 | * Writes statistics to log. 107 | * 108 | * @param stats 109 | * statistics to write 110 | */ 111 | private void printStats(Statistics stats) { 112 | log.info("Finished converting."); 113 | log.info("Processed " + stats.getTracksProcessed() 114 | + " tracks (total) of which " + stats.getTracksFailed() 115 | + " failed."); 116 | log.info("Processed " + stats.getPlaylistsProcessed() 117 | + " playlists of which " + stats.getPlaylistsFailed() 118 | + " failed."); 119 | log.info("Processed " + stats.getPlaylistTracksProcessed() 120 | + " tracks (playlist members) of which " 121 | + stats.getPlaylistTracksFailed() + " failed."); 122 | log.info("See log file for more info"); 123 | } 124 | 125 | /** 126 | * Make user confirm to use the "date added workaround" 127 | * 128 | * @return true if the user confirmed, false 129 | * otherwise 130 | */ 131 | private boolean confirmedWorkaround() { 132 | Scanner scanner = null; 133 | try { 134 | scanner = new Scanner(createSystemIn()); 135 | log.info("You used the option for using the workaround to set the \"date added\" in iTunes. As iTunes does not allow setting the \"date added\", this workaround sets the system clock to the desired date and then adds the song to iTunes. Make sure to"); 136 | log.info(" - start songbird2itunes with administration rights,"); 137 | log.info(" - either close iTunes or start iTunes as administrator, "); 138 | log.info(" - deactivate the automatic sync of windows with a time server for the progress of conversion to iTunes."); 139 | log.info(" - better not use your computer while the migration is running, because the system date might be invalid."); 140 | log.info("If you REALLY want to do this, type \"yes\". If not just press enter and restart without this option!"); 141 | if ("yes".equals(scanner.nextLine())) { 142 | return true; 143 | } 144 | } finally { 145 | if (scanner != null) { 146 | scanner.close(); 147 | } 148 | } 149 | return false; 150 | } 151 | 152 | /** 153 | * @return a new instance of System.in. Useful for testing. 154 | */ 155 | InputStream createSystemIn() { 156 | return System.in; 157 | } 158 | 159 | /** 160 | * @return a new instance of {@link Songbird2itunesMigration}. Useful for 161 | * testing. 162 | */ 163 | Songbird2itunesMigration createSongbird2itunes() { 164 | return new Songbird2itunesMigration(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | 4.0.0 20 | 21 | info.schnatterer 22 | songbird2itunes 23 | 1.2-SNAPSHOT 24 | songbird2itunes 25 | Migration tool to migrate a songbird database to itunes 26 | https://github.com/schnatterer/songbird2itunes 27 | 28 | 29 | 30 | Apache License, Version 2.0 31 | http://www.apache.org/licenses/LICENSE-2.0.txt 32 | 33 | 34 | 35 | 36 | scm:git:ssh://github.com/schnatterer/songbird2itunes.git 37 | scm:git:ssh://git@github.com/schnatterer/songbird2itunes.git 38 | https://github.com/schnatterer/songbird2itunes 39 | HEAD 40 | 41 | 42 | 43 | UTF-8 44 | yyyyMMddHHmmss 45 | v.${project.version} build ${maven.build.timestamp} 46 | 47 | 1.7.12 48 | 1.2.0 49 | 2.11 50 | 51 | 52 | 53 | 54 | songbirdDbApi4j-mvn-repo 55 | https://raw.github.com/schnatterer/songbirdDbApi4j/mvn-repo/ 56 | 57 | 58 | itunes4j-mvn-repo 59 | https://raw.github.com/schnatterer/itunes4j/mvn-repo/ 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-compiler-plugin 67 | 3.3 68 | 69 | 1.8 70 | 1.8 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-release-plugin 76 | 2.5.1 77 | 78 | -Prelease 79 | 80 | v.@{project.version} 81 | 82 | 83 | 84 | 85 | maven-deploy-plugin 86 | 2.8.1 87 | 88 | internal.repo::default::file://${project.build.directory}/release 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-assembly-plugin 95 | 2.5.4 96 | 97 | 98 | 99 | 100 | src/main/assembly/assembly.xml 101 | 102 | 103 | 104 | 105 | 106 | install 107 | 108 | single 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-jar-plugin 117 | 2.6 118 | 119 | 120 | ${project.artifactId} 121 | 122 | 123 | 124 | 125 | true 126 | 127 | lib/ 128 | info.schnatterer.songbird2itunes.Songbird2itunesApp 129 | 130 | 131 | ${buildNumber} 132 | 133 | 134 | 135 | 136 | 137 | 139 | com.mycila 140 | license-maven-plugin 141 | ${license-maven-plugin.version} 142 | 143 |

src/main/config/license-templates/APACHE-2.txt
144 | 145 | 2015 146 | Johannes Schnatterer 147 | 148 | 149 | LICENSE 150 | 151 | 152 | 153 | 154 | 155 | com.mycila 156 | license-maven-plugin-git 157 | 158 | ${license-maven-plugin.version} 159 | 160 | 161 | 162 | 163 | 164 | validate 165 | 166 | check 167 | 168 | 169 | 170 | 171 | 172 | 173 | org.jacoco 174 | jacoco-maven-plugin 175 | 0.7.5.201505241946 176 | 177 | 178 | initialize 179 | 180 | prepare-agent 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | info.schnatterer 191 | songbirdDbApi4j 192 | 2.0 193 | 194 | 195 | info.schnatterer 196 | itunes4j 197 | 2.0 198 | 199 | 200 | org.slf4j 201 | slf4j-api 202 | ${slf4j.version} 203 | 204 | 205 | ch.qos.logback 206 | logback-classic 207 | ${logback.version} 208 | 209 | 210 | ch.qos.logback 211 | logback-core 212 | ${logback.version} 213 | 214 | 215 | com.beust 216 | jcommander 217 | 1.48 218 | 219 | 220 | junit 221 | junit 222 | 4.13.1 223 | test 224 | 225 | 226 | org.mockito 227 | mockito-core 228 | 1.10.19 229 | test 230 | 231 | 232 | org.apache.ant 233 | ant 234 | 1.10.9 235 | test 236 | 237 | 238 | org.hamcrest 239 | hamcrest-junit 240 | 2.0.0.0 241 | test 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/main/java/info/schnatterer/songbird2itunes/migration/Songbird2itunesMigration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Johannes Schnatterer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package info.schnatterer.songbird2itunes.migration; 17 | 18 | import info.schnatterer.itunes4j.ITunes; 19 | import info.schnatterer.itunes4j.entity.Playlist; 20 | import info.schnatterer.itunes4j.entity.Rating; 21 | import info.schnatterer.itunes4j.entity.Track; 22 | import info.schnatterer.itunes4j.exception.ITunesException; 23 | import info.schnatterer.itunes4j.exception.NotModifiableException; 24 | import info.schnatterer.itunes4j.exception.WrongParameterException; 25 | import info.schnatterer.java.lang.SystemClock; 26 | import info.schnatterer.java.lang.SystemClock.SystemClockException; 27 | import info.schnatterer.java.lang.XLong; 28 | import info.schnatterer.java.util.Sets; 29 | import info.schnatterer.songbirddbapi4j.SongbirdDb; 30 | import info.schnatterer.songbirddbapi4j.domain.MediaItem; 31 | import info.schnatterer.songbirddbapi4j.domain.MemberMediaItem; 32 | import info.schnatterer.songbirddbapi4j.domain.Property; 33 | import info.schnatterer.songbirddbapi4j.domain.SimpleMediaList; 34 | 35 | import java.io.File; 36 | import java.io.IOException; 37 | import java.net.URI; 38 | import java.net.URISyntaxException; 39 | import java.sql.SQLException; 40 | import java.util.Collection; 41 | import java.util.Date; 42 | import java.util.List; 43 | import java.util.Optional; 44 | import java.util.Set; 45 | import java.util.stream.Collectors; 46 | 47 | import org.slf4j.Logger; 48 | import org.slf4j.LoggerFactory; 49 | 50 | public class Songbird2itunesMigration { 51 | /** SLF4J-Logger. */ 52 | private final Logger log = LoggerFactory.getLogger(getClass()); 53 | 54 | /** 55 | * Migrate all tracks and playlists from a songbird database to iTunes. 56 | * 57 | * @param songbirdDbFile 58 | * absolute File path to songbird database 59 | * @param exceptionRetries 60 | * After running into a {@link NotModifiableException} - amount 61 | * of times adding track is retried before exiting with an error. 62 | * @param setSystemDate 63 | * use workaround - try to set the date added in iTunes by 64 | * setting the system date to the date added and then adding the 65 | * track 66 | * @param playlistNames 67 | * migrate only the playlists and the tracks within playlists. 68 | * Don't migrat other tracks. If null or empty, all 69 | * playlists are migrated. 70 | * @param migratePlaylistsOnly 71 | * true migrates only the playlists and the tracks 72 | * within playlists. Don't migrate other tracks. 73 | * 74 | * @return statistics that keeps track of the number of migrated objects 75 | * 76 | * @throws SQLException 77 | * errors when querying source database 78 | * @throws ITunesException 79 | * errors when writing to target iTunes 80 | */ 81 | public Statistics migrate(String songbirdDbFile, int exceptionRetries, 82 | boolean setSystemDate, List playlistNames, 83 | boolean migratePlaylistsOnly) throws SQLException, ITunesException { 84 | // Create database wrapper instance 85 | SongbirdDb songbirdDb = createSongbirdDb(new File(songbirdDbFile)); 86 | // Create reference to iTunes 87 | ITunes iTunes = createItunes(); 88 | Optional systemClock = Optional.empty(); 89 | if (setSystemDate) { 90 | systemClock = Optional.of(new SystemClock()); 91 | } 92 | 93 | Statistics stats = new Statistics(); 94 | if (!migratePlaylistsOnly) { 95 | stats.merge(migrateTracks(songbirdDb, iTunes, exceptionRetries, 96 | systemClock)); 97 | } else { 98 | log.info("Migrating only tracks that are contained in playlists."); 99 | } 100 | 101 | /* 102 | * if migrating only playlists, set properties. If tracks have been 103 | * migrated (above) don't set them again (faster) 104 | */ 105 | stats.merge(migratePlaylists(songbirdDb, iTunes, exceptionRetries, 106 | migratePlaylistsOnly, systemClock, playlistNames)); 107 | 108 | return stats; 109 | } 110 | 111 | /** 112 | * Migrates playlists from songbird2iTunes. 113 | * 114 | * @param songbirdDb 115 | * songbird database wrapper 116 | * @param iTunes 117 | * iTunes wrapper After running into a 118 | * {@link NotModifiableException} - amount of times adding track 119 | * is retried before exiting with an error. 120 | * @param setProperties 121 | * true migrates properties lastPlayTime, 122 | * lastSkipTime, playCount, rating, skipCount 123 | * @param systemClock 124 | * system clock to set before adding the tracks to iTunes. If 125 | * {@link Optional#empty()} the system clock is not set. 126 | * @param requestedPlaylistNames 127 | * migrate only the playlists and the tracks within playlists. 128 | * Don't migrat other tracks. If null or empty, all 129 | * playlists are migrated. 130 | * 131 | * @return statistics about the migration 132 | * 133 | * @throws SQLException 134 | * errors when querying source database 135 | * @throws ITunesException 136 | * errors when writing to target iTunes 137 | */ 138 | private Statistics migratePlaylists(SongbirdDb songbirdDb, ITunes iTunes, 139 | int exceptionRetries, boolean setProperties, 140 | Optional systemClock, 141 | List requestedPlaylistNames) throws SQLException, 142 | ITunesException { 143 | Statistics stats = new Statistics(); 144 | 145 | // Find playlists in songbird 146 | List playlistsToMigrate = songbirdDb 147 | .getPlayLists(true, true) 148 | .stream() 149 | .sorted((p1, p2) -> p1 150 | .getList() 151 | .getProperty(Property.PROP_MEDIA_LIST_NAME) 152 | .compareTo( 153 | p2.getList().getProperty( 154 | Property.PROP_MEDIA_LIST_NAME))) 155 | .collect(Collectors.toList()); 156 | log.info(playlistsToMigrate.size() 157 | + " playlist(s) were found in songbird: " 158 | + extractPlaylistNames(playlistsToMigrate)); 159 | 160 | // Filter playlist as requested by the user 161 | if (requestedPlaylistNames != null && !requestedPlaylistNames.isEmpty()) { 162 | // Remove duplicates and sort 163 | requestedPlaylistNames = requestedPlaylistNames.stream().distinct() 164 | .sorted().collect(Collectors.toList()); 165 | log.info(requestedPlaylistNames.size() 166 | + " playlist(s) were requested by the user: " 167 | + toStringQuoted(requestedPlaylistNames)); 168 | 169 | Set requestedPlaylistNamesSetUpper = requestedPlaylistNames 170 | .stream() 171 | .map(playlistName -> playlistName.trim().toUpperCase()) 172 | .collect(Collectors.toSet()); 173 | Set songbirdPlaylistNamesSetUpper = playlistsToMigrate 174 | .stream() 175 | .map(playlist -> playlist.getList() 176 | .getProperty(Property.PROP_MEDIA_LIST_NAME).trim() 177 | .toUpperCase()).collect(Collectors.toSet()); 178 | 179 | // find playlists that are only in songbird but not requeted 180 | List ignoredPlaylists = Sets 181 | .relativeComplement( 182 | playlistsToMigrate, 183 | requestedPlaylistNamesSetUpper, 184 | playlist -> playlist.getList() 185 | .getProperty(Property.PROP_MEDIA_LIST_NAME) 186 | .trim().toUpperCase()) 187 | .map(playlist -> playlist.getList().getProperty( 188 | Property.PROP_MEDIA_LIST_NAME)).distinct().sorted() 189 | .collect(Collectors.toList()); 190 | if (!ignoredPlaylists.isEmpty()) { 191 | log.info(ignoredPlaylists.size() 192 | + " playlists are not migrated because they were not requested by the user: " 193 | + toStringQuoted(ignoredPlaylists)); 194 | } 195 | 196 | // find playlists thate are only requested but not in songbird 197 | List requestedPlaylistsNotFound = Sets 198 | .relativeComplement(requestedPlaylistNames, 199 | songbirdPlaylistNamesSetUpper, 200 | playlistName -> playlistName.trim().toUpperCase()) 201 | .distinct().sorted().collect(Collectors.toList()); 202 | if (!requestedPlaylistsNotFound.isEmpty()) { 203 | // This must be a warning 204 | log.warn(requestedPlaylistsNotFound.size() 205 | + " playlist(s) were requested by the user but not found in songbird: " 206 | + toStringQuoted(requestedPlaylistsNotFound)); 207 | } 208 | 209 | // Limit to playlists that are both: in songbird and requested 210 | playlistsToMigrate = Sets 211 | .intersection( 212 | playlistsToMigrate, 213 | requestedPlaylistNamesSetUpper, 214 | playlist -> playlist.getList() 215 | .getProperty(Property.PROP_MEDIA_LIST_NAME) 216 | .trim().toUpperCase()).distinct() 217 | .collect(Collectors.toList()); 218 | 219 | log.info(playlistsToMigrate.size() 220 | + " playlist(s) from the list were found in songbird and will be migrated: " 221 | + extractPlaylistNames(playlistsToMigrate)); 222 | } 223 | 224 | // Migrate filtered playlists 225 | for (SimpleMediaList playList : playlistsToMigrate) { 226 | String playlistName = playList.getList().getProperty( 227 | Property.PROP_MEDIA_LIST_NAME); 228 | 229 | stats.playlistProcessed(); 230 | Playlist iTunesplaylist = iTunes.createPlaylist(playlistName); 231 | log.info("Created Playlist #" + stats.getPlaylistsProcessed() 232 | + ": " + playlistName); 233 | for (MemberMediaItem member : playList.getMembers()) { 234 | stats.playlistTrackProcessed(); 235 | 236 | Optional optionalTrack = addTrack(iTunes, 237 | member.getMember(), exceptionRetries, setProperties, 238 | systemClock); 239 | if (optionalTrack.isPresent()) { 240 | printPlaylistTrack(stats.getPlaylistTracksProcessed(), 241 | playlistName, member.getMember()); 242 | iTunesplaylist.addTrack(optionalTrack.get()); 243 | } else { 244 | stats.playlistTrackFailed(); 245 | } 246 | } 247 | } 248 | return stats; 249 | } 250 | 251 | /** 252 | * Returns only the names of a list of {@link SimpleMediaList}s. 253 | * 254 | * @param playLists 255 | * the list of playlist objects 256 | * @return the names of the playlist objects 257 | */ 258 | private String extractPlaylistNames(List playLists) { 259 | return toStringQuoted(playLists 260 | .stream() 261 | .map(playlist -> playlist.getList().getProperty( 262 | Property.PROP_MEDIA_LIST_NAME)) 263 | .collect(Collectors.toList())); 264 | } 265 | 266 | /** 267 | * Convert a list of Strings to a single string, that contains the 268 | * comma-separated strings wrapped in quotes. 269 | * 270 | * @param list 271 | * the list to convert 272 | * 273 | * @return one concatenated string 274 | */ 275 | private String toStringQuoted(Collection list) { 276 | return list.stream().map(str -> "\"" + str + "\"") 277 | .collect(Collectors.joining(", ")); 278 | } 279 | 280 | /** 281 | * Migrates tracks from songbird2iTunes. 282 | * 283 | * @param songbirdDb 284 | * songbird database wrapper 285 | * @param iTunes 286 | * iTunes wrapper 287 | * @param exceptionRetries 288 | * After running into a {@link NotModifiableException} - amount 289 | * of times adding track is retried before exiting with an error. 290 | * @param systemClock 291 | * system clock to set before adding the tracks to iTunes. If 292 | * {@link Optional#empty()} the system clock is not set. 293 | * 294 | * @return statistics about the migration 295 | * 296 | * @throws SQLException 297 | * errors when querying source database 298 | * @throws ITunesException 299 | * errors when writing to target iTunes 300 | */ 301 | private Statistics migrateTracks(SongbirdDb songbirdDb, ITunes iTunes, 302 | int exceptionRetries, Optional systemClock) 303 | throws SQLException, ITunesException { 304 | Statistics stats = new Statistics(); 305 | 306 | // Query all tracks from songbird 307 | List tracks = songbirdDb.getAllTracks(); 308 | log.info("Found " + tracks.size() + " tracks"); 309 | 310 | try { 311 | for (MediaItem sbTrack : tracks) { 312 | stats.trackProcessed(); 313 | Optional optionalTrack = addTrack(iTunes, sbTrack, 314 | exceptionRetries, true, systemClock); 315 | if (optionalTrack.isPresent()) { 316 | printTrack(stats.getTracksProcessed(), optionalTrack.get(), 317 | sbTrack.getContentUrl()); 318 | } else { 319 | stats.trackFailed(); 320 | } 321 | } 322 | } finally { 323 | if (systemClock.isPresent()) { 324 | log.debug("Trying to resync system time from time server"); 325 | try { 326 | systemClock.get().resync(); 327 | } catch (SystemClockException e) { 328 | log.warn("Failed to resync system clock.", e); 329 | } 330 | } 331 | } 332 | return stats; 333 | } 334 | 335 | /** 336 | * Logs all details about a track: 337 | * 338 | * @param trackIndex 339 | * Number of the track 340 | * @param track 341 | * the track whose details to log 342 | * @param path 343 | * path to the track 344 | */ 345 | private void printPlaylistTrack(long playlistTrackIndex, 346 | String playlistName, MediaItem sbTrack) { 347 | log.info("Added playlist track #" + playlistTrackIndex 348 | + ": Playlist \"" + playlistName + "\" - Track " 349 | + sbTrack.getProperty(Property.PROP_ARTIST_NAME) + " - " 350 | + sbTrack.getProperty(Property.PROP_TRACK_NAME)); 351 | } 352 | 353 | /** 354 | * Logs all details about a track: 355 | * 356 | * @param trackIndex 357 | * Number of the track 358 | * @param track 359 | * the track whose details to log 360 | * @param path 361 | * path to the track 362 | * 363 | * @throws ITunesException 364 | */ 365 | private void printTrack(long trackIndex, Track track, String path) 366 | throws ITunesException { 367 | log.info("Added track #" + trackIndex + ": " + track.getArtist() 368 | + " - " + track.getName() + ": created=" + track.getDateAdded() 369 | + "; lastPlayed=" + track.getPlayedDate() + "; lastSkipTime=" 370 | + track.getSkippedDate() + "; playCount=" 371 | + track.getPlayedCount() + "; rating=" + track.getRating() 372 | + "; skipCount=" + track.getSkippedCount() + "; path=" + path); 373 | } 374 | 375 | /** 376 | * Add track to iTunes. If a {@link NotModifiableException} is throw, the 377 | * method calls itself nRetries recursively. If it still fails 378 | * an exception is re-thrown. 379 | * 380 | * @param iTunes 381 | * iTunes wrapper instance. 382 | * @param sbTrack 383 | * the source track to add to iTunes 384 | * @param exceptionRetries 385 | * After running into a {@link NotModifiableException} - amount 386 | * of times adding track is retried before exiting with an error. 387 | * @param setProperties 388 | * true migrates properties lastPlayTime, 389 | * lastSkipTime, playCount, rating, skipCount 390 | * @param systemClock 391 | * system clock to set before adding the tracks to iTunes. If 392 | * {@link Optional#empty()} the system clock is not set. 393 | * @return an instance of the added track or {@link Optional#empty()} in 394 | * case of error. If empty, a warning was logged. 395 | * 396 | * @throws ITunesException 397 | * after all retries have been used. 398 | */ 399 | private Optional addTrack(ITunes iTunes, MediaItem sbTrack, 400 | int exceptionRetries, boolean setProperties, 401 | Optional systemClock) throws ITunesException { 402 | Track iTunesTrack = null; 403 | try { 404 | // Get absolute path first (as this might fail) 405 | Optional absolutePath = toAbsolutePath(sbTrack); 406 | if (!absolutePath.isPresent()) { 407 | return Optional.empty(); 408 | } 409 | 410 | // Add track and wait for iTunes reference 411 | iTunesTrack = iTunes.addFile(absolutePath.get()); 412 | 413 | Date dateCreated = sbTrack.getDateCreated(); 414 | if (setProperties) { 415 | Date lastPlayTime = sbTrack 416 | .getPropertyAsDate(Property.PROP_LAST_PLAY_TIME); 417 | Date lastSkipTime = sbTrack 418 | .getPropertyAsDate(Property.PROP_LAST_SKIP_TIME); 419 | Long playCount = sbTrack 420 | .getPropertyAsLong(Property.PROP_PLAY_COUNT); 421 | Long rating = sbTrack.getPropertyAsLong(Property.PROP_RATING); 422 | Long skipCount = sbTrack 423 | .getPropertyAsLong(Property.PROP_SKIP_COUNT); 424 | 425 | if (systemClock.isPresent()) { 426 | /* 427 | * Changing the dateAdded is not possible via iTunes COM API 428 | * 429 | * Dirty Hack: Change the computer's system date, then add 430 | * the file. This will only work if the process runs as 431 | * administrator and iTunes is either not running or already 432 | * started as administrator! 433 | */ 434 | log.debug("Setting system time to " + dateCreated); 435 | try { 436 | systemClock.get().set(dateCreated); 437 | } catch (SystemClockException e) { 438 | log.warn( 439 | "Failed to set system clock to " + dateCreated, 440 | e); 441 | } 442 | } 443 | 444 | // Play count 445 | iTunesTrack.setPlayedCount(convertSongbirdLongValue(playCount)); 446 | // last played 447 | if (lastPlayTime != null) { 448 | iTunesTrack.setPlayedDate(lastPlayTime); 449 | } 450 | 451 | iTunesTrack.setRating(convertSongbirdRating(rating)); 452 | 453 | // Skip count 454 | iTunesTrack 455 | .setSkippedCount(convertSongbirdLongValue(skipCount)); 456 | // last skipped 457 | if (lastSkipTime != null) { 458 | iTunesTrack.setSkippedDate(lastSkipTime); 459 | } 460 | } 461 | return Optional.of(iTunesTrack); 462 | } catch (IOException e) { 463 | log.warn( 464 | "File not added by iTunes. File corrupt, missing or not supported by iTunes? Skipping file: " 465 | + sbTrack.getContentUrl(), e); 466 | } catch (WrongParameterException e) { 467 | log.warn( 468 | "File not added by iTunes. Unsupported type? Skipping file: " 469 | + sbTrack.getContentUrl(), e); 470 | // TODO try to convert? 471 | } catch (NotModifiableException e) { 472 | return retryAdding(e, iTunes, sbTrack, exceptionRetries, 473 | systemClock); 474 | } 475 | return Optional.empty(); 476 | } 477 | 478 | /** 479 | * Migrates a songbird track to an absolute URL in the file system. If not a 480 | * valid file an appropriate warning is logged. 481 | * 482 | * @param sbTrack 483 | * the songbird track whose absolute path is required 484 | * 485 | * @return the absolute path of the track or an empty result if invalid URI 486 | * or not a file URI. 487 | * 488 | */ 489 | private Optional toAbsolutePath(MediaItem sbTrack) { 490 | URI uri = null; 491 | try { 492 | uri = new URI(sbTrack.getContentUrl()); 493 | return Optional.of(new File(uri).getCanonicalPath()); 494 | } catch (URISyntaxException e) { 495 | log.warn( 496 | "Error adding track iTunes, invalid URI: " 497 | + sbTrack.getContentUrl(), e); 498 | return Optional.empty(); 499 | } catch (IllegalArgumentException e) { 500 | log.warn("Songbird track URI is not a valid path within the file system. Error: " 501 | + e.getMessage() 502 | + ". Skipping track: " 503 | + sbTrack.getContentUrl()); 504 | return Optional.empty(); 505 | } catch (IOException e) { 506 | log.warn("Songbird track URI cannot be found within the file system. Error: " 507 | + e.getMessage() 508 | + ". Skipping track: " 509 | + sbTrack.getContentUrl()); 510 | return Optional.empty(); 511 | } 512 | } 513 | 514 | /** 515 | * Retries calling {@link #addTrack(ITunes, MediaItem, Statistics, int)} 516 | * after a {@link ITunesException}. This is done for nRetries 517 | * times, before giving up and logging a warning. Why? iTunes seems to 518 | * return errors and reconsiders on retry. 519 | * 520 | * An example is the "a0040203" ({@link NotModifiableException}) error in 521 | * iTunes. 522 | * 523 | * This exception might occur right after a track has been added but iTunes 524 | * (for some reasons) won't let us modify it for some more milliseconds. 525 | * Maybe it parses artwork or goes fishing. 526 | * 527 | * After handling the exception (~ 500ms) modifying the track will most 528 | * likely work, because iTunes seems to have finished what it did before. So 529 | * just keep trying some more times. If the error persists, throw Exception. 530 | * 531 | * @param e 532 | * exception that might be a "a0040203" 533 | * @param iTunes 534 | * reference to the iTunes wrapper 535 | * @param sbTrack 536 | * reference to the songbird track 537 | * @param nRetries 538 | * amount of retries left 539 | * @param systemClock 540 | * system clock to set before adding the tracks to iTunes. If 541 | * {@link Optional#empty()} the system clock is not set. 542 | * @return 543 | * 544 | * @throws ITunesException 545 | * if thrown by 546 | * {@link #addTrack(ITunes, MediaItem, Statistics, int, boolean)} 547 | */ 548 | private Optional retryAdding(ITunesException e, ITunes iTunes, 549 | MediaItem sbTrack, int nRetries, Optional systemClock) 550 | throws ITunesException { 551 | if (nRetries > 0) { 552 | log.debug( 553 | "Track was added, but error setting attributes. Retrying " 554 | + nRetries + " more times. File: " 555 | + sbTrack.getContentUrl(), e); 556 | return addTrack(iTunes, sbTrack, nRetries - 1, true, systemClock); 557 | } else { 558 | log.warn( 559 | "Unable set track attributes, tried multiple times without luck. Skipping. You might manually add File: " 560 | + sbTrack.getContentUrl(), e); 561 | return Optional.empty(); 562 | } 563 | } 564 | 565 | /** 566 | * Factory method for {@link SongbirdDb} API. Useful for testing. 567 | * 568 | * @param songbirdDbFile 569 | * the path to the database 570 | * @return an instance of the songbirdDb API 571 | */ 572 | protected SongbirdDb createSongbirdDb(File songbirdDbFile) { 573 | return new SongbirdDb(songbirdDbFile.getAbsolutePath()); 574 | } 575 | 576 | /** 577 | * Factory method for {@link ITunes} wrapper. Useful for testing. 578 | * 579 | * @return a new instance of {@link ITunes} 580 | */ 581 | protected ITunes createItunes() { 582 | return new ITunes(); 583 | } 584 | 585 | /** 586 | * Songbird database might return a null that means zero, in 587 | * addition iTunes can only handle integers. This method provides this 588 | * conversion, returning a primitive long value. 589 | * 590 | * @param longValue 591 | * value to convert 592 | * @return a primitive (non-null) instance of 593 | * longValue 594 | */ 595 | protected int convertSongbirdLongValue(Long longValue) { 596 | if (longValue == null) { 597 | return 0; 598 | } 599 | 600 | return new XLong(longValue).toInt(); 601 | } 602 | 603 | /** 604 | * Converts a songbird rating (null or 0..5) to an iTunes 605 | * {@link Rating} object. 606 | * 607 | * @param rating 608 | * the rating read from songbird 609 | * @return an iTunes {@link Rating} object 610 | */ 611 | protected Rating convertSongbirdRating(Long rating) { 612 | return Rating.fromStars(convertSongbirdLongValue(rating)); 613 | } 614 | 615 | public static class Statistics { 616 | private long tracksProcessed = 0; 617 | private long tracksFailed = 0; 618 | private long playlistTracksProcessed = 0; 619 | private long playlistTracksFailed = 0; 620 | private long playlistsProcessed = 0; 621 | private long playlistsFailed = 0; 622 | 623 | private void trackProcessed() { 624 | tracksProcessed++; 625 | } 626 | 627 | private void trackFailed() { 628 | tracksFailed++; 629 | } 630 | 631 | private void playlistTrackProcessed() { 632 | playlistTracksProcessed++; 633 | } 634 | 635 | private void playlistTrackFailed() { 636 | playlistTracksFailed++; 637 | } 638 | 639 | private void playlistProcessed() { 640 | playlistsProcessed++; 641 | } 642 | 643 | // private void playlistFailed() { 644 | // playlistsFailed++; 645 | // } 646 | 647 | public long getTracksFailed() { 648 | return tracksFailed; 649 | } 650 | 651 | public long getTracksProcessed() { 652 | return tracksProcessed; 653 | } 654 | 655 | public long getPlaylistTracksProcessed() { 656 | return playlistTracksProcessed; 657 | } 658 | 659 | public long getPlaylistTracksFailed() { 660 | return playlistTracksFailed; 661 | } 662 | 663 | public long getPlaylistsProcessed() { 664 | return playlistsProcessed; 665 | } 666 | 667 | public long getPlaylistsFailed() { 668 | return playlistsFailed; 669 | } 670 | 671 | private void merge(Statistics stats) { 672 | this.tracksProcessed += stats.tracksProcessed; 673 | this.tracksFailed += stats.tracksFailed; 674 | this.playlistTracksProcessed += stats.playlistTracksProcessed; 675 | this.playlistTracksFailed += stats.playlistTracksFailed; 676 | this.playlistsProcessed += stats.playlistsProcessed; 677 | this.playlistsFailed += stats.playlistsFailed; 678 | } 679 | } 680 | } 681 | --------------------------------------------------------------------------------