No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2011 Jonathan Matthew <jonathan@d14n.org>
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 2 of the License, or
8 * (at your option) any later version.
9 *
10 * The Rhythmbox authors hereby grant permission for non-GPL compatible
11 * GStreamer plugins to be used and distributed together with GStreamer
12 * and Rhythmbox. This permission is above and beyond the permissions granted
13 * by the GPL license by which Rhythmbox is covered. If you modify this code
14 * you may extend this exception to your version of the code, but you are not
15 * obligated to do so. If you do not wish to do so, delete this exception
16 * statement from your version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 *
27 */
28
29 #include <config.h>
30
31 #include <stdlib.h>
32 #include <string.h>
33
34 #include <gst/gst.h>
35 #include <gst/pbutils/encoding-target.h>
36 #include <glib/gi18n.h>
37
38 #include <sources/rb-transfer-target.h>
39 #include <shell/rb-track-transfer-queue.h>
40 #include <backends/rb-encoder.h>
41 #include <lib/rb-debug.h>
42 #include <lib/rb-file-helpers.h>
43 #include <widgets/rb-dialog.h>
44
45 /* arbitrary length limit for file extensions */
46 #define EXTENSION_LENGTH_LIMIT 8
47
48 G_DEFINE_INTERFACE (RBTransferTarget, rb_transfer_target, 0)
49
50 /**
51 * SECTION:rb-transfer-target
52 * @short_description: interface for sources that can receive track transfers
53 * @include: rb-transfer-target.h
54 *
55 * Sources that can accept track transfers should implement this interface
56 * and call the associated functions to perform transfers. The source
57 * needs to be able to construct target URIs for transfers, and can optionally
58 * perform its own processing after transfers have finished. The source
59 * must also provide a #GstEncodingTarget that describes the formats it
60 * accepts.
61 */
62
63
64 /**
65 * rb_transfer_target_build_dest_uri:
66 * @target: an #RBTransferTarget
67 * @entry: a #RhythmDBEntry being transferred
68 * @media_type: destination media type
69 * @extension: extension associated with destination media type
70 *
71 * Constructs a URI to use as the destination for a transfer or transcoding
72 * operation. The URI may be on the device itself, if the device is mounted
73 * into the normal filesystem or through gvfs, or it may be a temporary
74 * location used to store the file before uploading it to the device.
75 *
76 * The destination URI should conform to the device's normal URI format,
77 * and should use the provided extension instead of the extension from
78 * the source entry.
79 *
80 * Return value: constructed URI
81 */
82 char *
83 rb_transfer_target_build_dest_uri (RBTransferTarget *target,
84 RhythmDBEntry *entry,
85 const char *media_type,
86 const char *extension)
87 {
88 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
89 char *uri;
90
91 uri = iface->build_dest_uri (target, entry, media_type, extension);
92 if (uri != NULL) {
93 char *sane_uri;
94
95 sane_uri = rb_sanitize_uri_for_filesystem (uri);
96 g_return_val_if_fail (sane_uri != NULL, NULL);
97 g_free (uri);
98 uri = sane_uri;
99
100 rb_debug ("built dest uri for media type '%s', extension '%s': %s",
101 media_type, extension, uri);
102 } else {
103 rb_debug ("couldn't build dest uri for media type %s, extension %s",
104 media_type, extension);
105 }
106
107 return uri;
108 }
109
110 /**
111 * rb_transfer_target_track_added:
112 * @target: an #RBTransferTarget
113 * @entry: the source #RhythmDBEntry for the transfer
114 * @uri: the destination URI
115 * @filesize: size of the destination file
116 * @media_type: media type of the destination file
117 *
118 * This is called when a transfer to the target has completed.
119 * If the source's @track_added method returns %TRUE, the destination
120 * URI will be added to the database using the entry type for the device.
121 *
122 * If the target uses a temporary area as the destination for transfers,
123 * it can instead upload the destination file to the device and create an
124 * entry for it, then return %FALSE.
125 */
126 void
127 rb_transfer_target_track_added (RBTransferTarget *target,
128 RhythmDBEntry *entry,
129 const char *uri,
130 guint64 filesize,
131 const char *media_type)
132 {
133 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
134 gboolean add_to_db = TRUE;
135
136 if (iface->track_added)
137 add_to_db = iface->track_added (target, entry, uri, filesize, media_type);
138
139 if (add_to_db) {
140 RhythmDBEntryType *entry_type;
141 RhythmDB *db;
142 RBShell *shell;
143
144 g_object_get (target, "shell", &shell, NULL);
145 g_object_get (shell, "db", &db, NULL);
146 g_object_unref (shell);
147
148 g_object_get (target, "entry-type", &entry_type, NULL);
149 rhythmdb_add_uri_with_types (db, uri, entry_type, NULL, NULL);
150 g_object_unref (entry_type);
151
152 g_object_unref (db);
153 }
154 }
155
156 /**
157 * rb_transfer_target_track_add_error:
158 * @target: an #RBTransferTarget
159 * @entry: the source #RhythmDBEntry for the transfer
160 * @uri: the destination URI
161 * @error: the transfer error information
162 *
163 * This is called when a transfer fails. If the source's
164 * impl_track_add_error implementation returns %TRUE, an error dialog
165 * will be displayed to the user containing the error message, unless
166 * the error indicates that the destination file already exists.
167 */
168 void
169 rb_transfer_target_track_add_error (RBTransferTarget *target,
170 RhythmDBEntry *entry,
171 const char *uri,
172 GError *error)
173 {
174 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
175 gboolean show_dialog = TRUE;
176
177 /* hrm, want the subclass to decide whether to display the error and
178 * whether to cancel the batch (may have some device-specific errors?)
179 *
180 * for now we'll just cancel on the most common things..
181 */
182 if (iface->track_add_error)
183 show_dialog = iface->track_add_error (target, entry, uri, error);
184
185 if (show_dialog) {
186 if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
187 rb_debug ("not displaying 'file exists' error for %s", uri);
188 } else {
189 rb_error_dialog (NULL, _("Error transferring track"), "%s", error->message);
190 }
191 }
192 }
193
194 /**
195 * rb_transfer_target_get_format_descriptions:
196 * @target: an #RBTransferTarget
197 *
198 * Returns a #GList of allocated media format descriptions for
199 * the formats supported by the target. The list and the strings
200 * it holds must be freed by the caller.
201 *
202 * Return value: (element-type utf8) (transfer full): list of descriptions.
203 */
204 GList *
205 rb_transfer_target_get_format_descriptions (RBTransferTarget *target)
206 {
207 GstEncodingTarget *enctarget;
208 const GList *l;
209 GList *desc = NULL;
210 g_object_get (target, "encoding-target", &enctarget, NULL);
211 if (enctarget != NULL) {
212 for (l = gst_encoding_target_get_profiles (enctarget); l != NULL; l = l->next) {
213 GstEncodingProfile *profile = l->data;
214 desc = g_list_append (desc, g_strdup (gst_encoding_profile_get_description (profile)));
215 }
216 gst_encoding_target_unref (enctarget);
217 }
218 return desc;
219 }
220
221 /**
222 * rb_transfer_target_should_transfer:
223 * @target: an #RBTransferTarget
224 * @entry: a #RhythmDBEntry to consider transferring
225 *
226 * Checks whether @entry should be transferred to the target.
227 * The target can check whether a matching entry already exists on the device,
228 * for instance. @rb_transfer_target_check_duplicate may form part of
229 * an implementation. If this method returns %FALSE, the entry
230 * will be skipped.
231 *
232 * Return value: %TRUE if the entry should be transferred to the target
233 */
234 gboolean
235 rb_transfer_target_should_transfer (RBTransferTarget *target, RhythmDBEntry *entry)
236 {
237 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
238
239 return iface->should_transfer (target, entry);
240 }
241
242 /**
243 * rb_transfer_target_check_category:
244 * @target: an #RBTransferTarget
245 * @entry: a #RhythmDBEntry to check
246 *
247 * This checks that the entry type of @entry is in a suitable
248 * category for transfer. This can be used to implement
249 * @should_transfer.
250 *
251 * Return value: %TRUE if the entry is in a suitable category
252 */
253 gboolean
254 rb_transfer_target_check_category (RBTransferTarget *target, RhythmDBEntry *entry)
255 {
256 RhythmDBEntryCategory cat;
257 RhythmDBEntryType *entry_type;
258
259 entry_type = rhythmdb_entry_get_entry_type (entry);
260 g_object_get (entry_type, "category", &cat, NULL);
261 return (cat == RHYTHMDB_ENTRY_NORMAL);
262 }
263
264 /**
265 * rb_transfer_target_check_duplicate:
266 * @target: an #RBTransferTarget
267 * @entry: a #RhythmDBEntry to check
268 *
269 * This checks for an existing entry in the target that matches
270 * the title, album, artist, and track number of the entry being
271 * considered. This can be used to implement @should_transfer.
272 *
273 * Return value: %TRUE if the entry already exists on the target.
274 */
275 gboolean
276 rb_transfer_target_check_duplicate (RBTransferTarget *target, RhythmDBEntry *entry)
277 {
278 RhythmDBEntryType *entry_type;
279 RhythmDB *db;
280 RBShell *shell;
281 const char *title;
282 const char *album;
283 const char *artist;
284 gulong track_number;
285 GtkTreeModel *query_model;
286 GtkTreeIter iter;
287 gboolean is_dup;
288
289 g_object_get (target, "shell", &shell, "entry-type", &entry_type, NULL);
290 g_object_get (shell, "db", &db, NULL);
291 g_object_unref (shell);
292
293 query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
294 title = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
295 album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
296 artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST);
297 track_number = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_TRACK_NUMBER);
298 rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
299 RHYTHMDB_QUERY_PROP_EQUALS,
300 RHYTHMDB_PROP_TYPE, entry_type,
301 RHYTHMDB_QUERY_PROP_EQUALS,
302 RHYTHMDB_PROP_ARTIST, artist,
303 RHYTHMDB_QUERY_PROP_EQUALS,
304 RHYTHMDB_PROP_ALBUM, album,
305 RHYTHMDB_QUERY_PROP_EQUALS,
306 RHYTHMDB_PROP_TITLE, title,
307 RHYTHMDB_QUERY_PROP_EQUALS,
308 RHYTHMDB_PROP_TRACK_NUMBER, track_number,
309 RHYTHMDB_QUERY_END);
310
311 is_dup = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (query_model), &iter);
312 g_object_unref (entry_type);
313 g_object_unref (query_model);
314 g_object_unref (db);
315 if (is_dup) {
316 rb_debug ("not transferring %lu - %s - %s - %s as already present",
317 track_number, title, album, artist);
318 }
319 return is_dup;
320 }
321
322 static gboolean
323 default_should_transfer (RBTransferTarget *target, RhythmDBEntry *entry)
324 {
325 if (rb_transfer_target_check_category (target, entry) == FALSE)
326 return FALSE;
327
328 return (rb_transfer_target_check_duplicate (target, entry) == FALSE);
329 }
330
331 static char *
332 get_dest_uri_cb (RBTrackTransferBatch *batch,
333 RhythmDBEntry *entry,
334 const char *mediatype,
335 const char *extension,
336 RBTransferTarget *target)
337 {
338 char *free_ext = NULL;
339 char *uri;
340
341 /* make sure the extension isn't ludicrously long */
342 if (extension == NULL) {
343 extension = "";
344 } else if (strlen (extension) > EXTENSION_LENGTH_LIMIT) {
345 free_ext = g_strdup (extension);
346 free_ext[EXTENSION_LENGTH_LIMIT] = '\0';
347 extension = free_ext;
348 }
349 uri = rb_transfer_target_build_dest_uri (target, entry, mediatype, extension);
350 g_free (free_ext);
351 return uri;
352 }
353
354 static void
355 track_done_cb (RBTrackTransferBatch *batch,
356 RhythmDBEntry *entry,
357 const char *dest,
358 guint64 dest_size,
359 const char *dest_mediatype,
360 GError *error,
361 RBTransferTarget *target)
362 {
363 if (error != NULL) {
364 if (g_error_matches (error, RB_ENCODER_ERROR, RB_ENCODER_ERROR_OUT_OF_SPACE) ||
365 g_error_matches (error, RB_ENCODER_ERROR, RB_ENCODER_ERROR_DEST_READ_ONLY)) {
366 rb_debug ("fatal transfer error: %s", error->message);
367 rb_track_transfer_batch_cancel (batch);
368 }
369 rb_transfer_target_track_add_error (target, entry, dest, error);
370 } else {
371 rb_transfer_target_track_added (target, entry, dest, dest_size, dest_mediatype);
372 }
373 }
374
375 /**
376 * rb_transfer_target_transfer:
377 * @target: an #RBTransferTarget
378 * @entries: a #GList of entries to transfer
379 * @defer: if %TRUE, don't start the transfer until
380 *
381 * Starts tranferring @entries to the target. This returns the
382 * #RBTrackTransferBatch that it starts, so the caller can track
383 * the progress of the transfer, or NULL if the target doesn't
384 * want any of the entries.
385 *
386 * Return value: (transfer full): an #RBTrackTransferBatch, or NULL
387 */
388 RBTrackTransferBatch *
389 rb_transfer_target_transfer (RBTransferTarget *target, GList *entries, gboolean defer)
390 {
391 RBTrackTransferQueue *xferq;
392 RBShell *shell;
393 GList *l;
394 RhythmDBEntryType *our_entry_type;
395 RBTrackTransferBatch *batch;
396 gboolean start_batch = FALSE;
397
398 g_object_get (target,
399 "shell", &shell,
400 "entry-type", &our_entry_type,
401 NULL);
402 g_object_get (shell, "track-transfer-queue", &xferq, NULL);
403 g_object_unref (shell);
404
405 batch = g_object_steal_data (G_OBJECT (target), "transfer-target-batch");
406
407 if (batch == NULL) {
408 batch = rb_track_transfer_batch_new (NULL, NULL, G_OBJECT (target));
409
410 g_signal_connect_object (batch, "get-dest-uri", G_CALLBACK (get_dest_uri_cb), target, 0);
411 g_signal_connect_object (batch, "track-done", G_CALLBACK (track_done_cb), target, 0);
412 } else {
413 start_batch = TRUE;
414 }
415
416 for (l = entries; l != NULL; l = l->next) {
417 RhythmDBEntry *entry;
418 RhythmDBEntryType *entry_type;
419 const char *location;
420
421 entry = (RhythmDBEntry *)l->data;
422 location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
423 entry_type = rhythmdb_entry_get_entry_type (entry);
424
425 if (entry_type != our_entry_type) {
426 if (rb_transfer_target_should_transfer (target, entry)) {
427 rb_debug ("pasting entry %s", location);
428 rb_track_transfer_batch_add (batch, entry);
429 start_batch = TRUE;
430 } else {
431 rb_debug ("target doesn't want entry %s", location);
432 }
433 } else {
434 rb_debug ("can't copy entry %s from the target to itself", location);
435 }
436 }
437 g_object_unref (our_entry_type);
438
439 if (start_batch) {
440 if (defer) {
441 g_object_set_data_full (G_OBJECT (target), "transfer-target-batch", g_object_ref (batch), g_object_unref);
442 } else {
443 GstEncodingTarget *encoding_target;
444 g_object_get (target, "encoding-target", &encoding_target, NULL);
445 g_object_set (batch, "encoding-target", encoding_target, NULL);
446 gst_encoding_target_unref (encoding_target);
447
448 rb_track_transfer_queue_start_batch (xferq, batch);
449 }
450 } else {
451 g_object_unref (batch);
452 batch = NULL;
453 }
454 g_object_unref (xferq);
455 return batch;
456 }
457
458
459 static void
460 rb_transfer_target_default_init (RBTransferTargetInterface *interface)
461 {
462 interface->should_transfer = default_should_transfer;
463
464 g_object_interface_install_property (interface,
465 gst_param_spec_mini_object ("encoding-target",
466 "encoding target",
467 "GstEncodingTarget",
468 GST_TYPE_ENCODING_TARGET,
469 G_PARAM_READWRITE));
470 }