View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU 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 General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.ambient.collection.sync;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.text.ParseException;
25  import java.util.Collection;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.Set;
29  
30  import android.media.MediaScannerConnection;
31  
32  import sk.baka.ambient.collection.ICollection;
33  import sk.baka.ambient.collection.TrackMetadataBean;
34  import sk.baka.ambient.collection.file.AbstractAudio;
35  import sk.baka.ambient.commons.IOUtils;
36  import sk.baka.ambient.commons.MiscUtils;
37  import sk.baka.ambient.commons.TagFormatter;
38  
39  /***
40   * Utility class.
41   * 
42   * @author Martin Vysny
43   */
44  public final class SyncUtils {
45  	/***
46  	 * produces a relative path where the music file should be stored. Must
47  	 * produce valid paths! Paths are created automatically.
48  	 */
49  	public final TagFormatter pathFormatter;
50  	/***
51  	 * This is the reference collection, i.e. tracks which are not to be
52  	 * synchronized.
53  	 */
54  	public final ICollection reference;
55  	/***
56  	 * Download excess tracks from this collection.
57  	 */
58  	public final ICollection downloadFrom;
59  
60  	/***
61  	 * Connection to the media scanner.
62  	 */
63  	public final MediaScannerConnection msc;
64  	
65  	/***
66  	 * Returns a new instance of a default tag formatter. It outputs strings in
67  	 * the following form:
68  	 * <code>sync/artist/year album/tracknum title.ext</code>.
69  	 * 
70  	 * @return non-<code>null</code> formatter instance.
71  	 */
72  	public static TagFormatter newPathFormatter() {
73  		try {
74  			return new TagFormatter(
75  					"sync/{%artist/}{{%year }%album/}{%track }%title");
76  		} catch (ParseException e) {
77  			throw new RuntimeException(e);
78  		}
79  	}
80  
81  	/***
82  	 * @param reference
83  	 *            This is the reference collection, i.e. tracks which are not to
84  	 *            be synchronized.
85  	 * @param downloadFrom
86  	 *            Download excess tracks from this collection.
87  	 * @param pathFormatter
88  	 *            produces a relative path where the music file should be
89  	 *            stored. Must produce valid paths! Paths are created
90  	 *            automatically.
91  	 * @param msc
92  	 *            after a successful track download the track will be reported
93  	 *            to Android scanner.
94  	 */
95  	public SyncUtils(final ICollection reference,
96  			final ICollection downloadFrom, final TagFormatter pathFormatter, final MediaScannerConnection msc) {
97  		this.reference = reference;
98  		this.downloadFrom = downloadFrom;
99  		this.pathFormatter = pathFormatter;
100 		this.msc = msc;
101 	}
102 
103 	/***
104 	 * Removes local tracks from given collection.
105 	 * 
106 	 * @param tracks
107 	 *            the track list. Removes local tracks from this collection.
108 	 * @param localTracks
109 	 *            a list of local tracks.
110 	 */
111 	public static void removeLocalTracks(Collection<TrackMetadataBean> tracks,
112 			Collection<? extends TrackMetadataBean> localTracks) {
113 		final Set<SameTrack> local = new HashSet<SameTrack>(localTracks.size());
114 		for (final TrackMetadataBean track : localTracks) {
115 			local.add(new SameTrack(track));
116 		}
117 		for (final Iterator<TrackMetadataBean> download = tracks.iterator(); download
118 				.hasNext();) {
119 			final SameTrack track = new SameTrack(download.next());
120 			if (local.contains(track)) {
121 				download.remove();
122 			}
123 		}
124 	}
125 
126 	private static final class SameTrack {
127 		private final TrackMetadataBean track;
128 
129 		/***
130 		 * Creates the comparator.
131 		 * 
132 		 * @param track
133 		 *            the track.
134 		 */
135 		public SameTrack(final TrackMetadataBean track) {
136 			this.track = track;
137 		}
138 
139 		@Override
140 		public boolean equals(Object o) {
141 			if (o == this) {
142 				return true;
143 			}
144 			if (!(o instanceof SameTrack)) {
145 				return false;
146 			}
147 			final TrackMetadataBean other = ((SameTrack) o).track;
148 			return MiscUtils.nullEquals(track.getDisplayableName(), other
149 					.getDisplayableName())
150 					&& MiscUtils.nullEquals(track.getAlbum(), other.getAlbum())
151 					&& MiscUtils.nullEquals(track.getArtist(), other
152 							.getArtist());
153 		}
154 
155 		@Override
156 		public int hashCode() {
157 			int result = MiscUtils.hashCode(track.getDisplayableName());
158 			result = result * 1001 + MiscUtils.hashCode(track.getAlbum());
159 			result = result * 1001 + MiscUtils.hashCode(track.getArtist());
160 			return result;
161 		}
162 	}
163 
164 	/***
165 	 * Creates a new track with invalid characters removed from title, album
166 	 * name, artist name, genre, year released and track number.
167 	 * 
168 	 * @param track
169 	 *            the track to fix
170 	 * @return fixed track.
171 	 */
172 	public static TrackMetadataBean removeSlashes(final TrackMetadataBean track) {
173 		final TrackMetadataBean.Builder b = new TrackMetadataBean.Builder();
174 		b.getData(track);
175 		b.title = removeSlashes(b.title);
176 		b.album = removeSlashes(b.album);
177 		b.artist = removeSlashes(b.artist);
178 		b.genre = removeSlashes(b.genre);
179 		b.yearReleased = removeSlashes(b.yearReleased);
180 		b.trackNumber = removeSlashes(b.trackNumber);
181 		return b.build(-1);
182 	}
183 
184 	/***
185 	 * Since /sdcard is most probably a fat32 drive, we have to remove several
186 	 * characters beside slash.
187 	 * 
188 	 * @param str
189 	 *            the string to remove invalid characters from.
190 	 * @return string having all invalid characters replaced by _
191 	 */
192 	public static String removeSlashes(final String str) {
193 		if (str == null) {
194 			return null;
195 		}
196 		String result = str;
197 		for (final char c : INVALID_CHARS) {
198 			result = result.replace(c, '_');
199 		}
200 		return result;
201 	}
202 
203 	private final static char[] INVALID_CHARS = "///:*?<>|".toCharArray();
204 
205 	/***
206 	 * Downloads given track.
207 	 * @param rootPath prepend the formatted name with this path. Must not end with a slash.
208 	 * @param track the track. Slashes are removed automatically.
209 	 * @throws IOException
210 	 */
211 	public void downloadTrack(final String rootPath, final TrackMetadataBean track) throws IOException {
212 		final TrackMetadataBean t = removeSlashes(track);
213 		final AbstractAudio source = AbstractAudio.fromUri(track.getLocation());
214 		final String targetFileName = rootPath + "/"
215 				+ pathFormatter.format(t)
216 				+ IOUtils.getExt(t.getLocation()).toLowerCase();
217 		final File target = new File(targetFileName);
218 		if (target.exists()) {
219 			// the track already exists... skip
220 			msc.scanFile(targetFileName, null);
221 			return;
222 		}
223 		final File parentDir = target.getParentFile();
224 		if (!parentDir.exists()) {
225 			if (!parentDir.mkdirs()) {
226 				throw new IOException("Failed to create directory "
227 						+ parentDir.toString());
228 			}
229 		}
230 		try {
231 			IOUtils.copy(source.openInputStream(), new FileOutputStream(target),
232 					8192);
233 			msc.scanFile(targetFileName, null);
234 			// register the track in the android collection manager
235 		} catch (RuntimeException ex) {
236 			target.delete();
237 			throw ex;
238 		} catch (IOException ex) {
239 			target.delete();
240 			throw ex;
241 		}
242 	}
243 }