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
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
235 } catch (RuntimeException ex) {
236 target.delete();
237 throw ex;
238 } catch (IOException ex) {
239 target.delete();
240 throw ex;
241 }
242 }
243 }