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, see <http://www.gnu.org/licenses/>.
25 *
26 */
27
28 #include <config.h>
29
30 #include <stdlib.h>
31 #include <string.h>
32
33 #include <gst/gst.h>
34 #include <gst/pbutils/encoding-target.h>
35 #include <glib/gi18n.h>
36
37 #include <sources/rb-transfer-target.h>
38 #include <shell/rb-track-transfer-queue.h>
39 #include <backends/rb-encoder.h>
40 #include <lib/rb-debug.h>
41 #include <lib/rb-file-helpers.h>
42 #include <widgets/rb-dialog.h>
43 #include <shell/rb-task-list.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 *
rb_transfer_target_build_dest_uri(RBTransferTarget * target,RhythmDBEntry * entry,const char * media_type,const char * extension)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 rb_debug ("built dest uri for media type '%s', extension '%s': %s",
94 media_type, extension, uri);
95 } else {
96 rb_debug ("couldn't build dest uri for media type %s, extension %s",
97 media_type, extension);
98 }
99
100 return uri;
101 }
102
103 /**
104 * rb_transfer_target_track_added:
105 * @target: an #RBTransferTarget
106 * @entry: the source #RhythmDBEntry for the transfer
107 * @uri: the destination URI
108 * @filesize: size of the destination file
109 * @media_type: media type of the destination file
110 *
111 * This is called when a transfer to the target has completed.
112 * If the source's @track_added method returns %TRUE, the destination
113 * URI will be added to the database using the entry type for the device.
114 *
115 * If the target uses a temporary area as the destination for transfers,
116 * it can instead upload the destination file to the device and create an
117 * entry for it, then return %FALSE.
118 */
119 void
rb_transfer_target_track_added(RBTransferTarget * target,RhythmDBEntry * entry,const char * uri,guint64 filesize,const char * media_type)120 rb_transfer_target_track_added (RBTransferTarget *target,
121 RhythmDBEntry *entry,
122 const char *uri,
123 guint64 filesize,
124 const char *media_type)
125 {
126 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
127 gboolean add_to_db = TRUE;
128
129 if (iface->track_added)
130 add_to_db = iface->track_added (target, entry, uri, filesize, media_type);
131
132 if (add_to_db) {
133 RhythmDBEntryType *entry_type;
134 RhythmDB *db;
135 RBShell *shell;
136
137 g_object_get (target, "shell", &shell, NULL);
138 g_object_get (shell, "db", &db, NULL);
139 g_object_unref (shell);
140
141 g_object_get (target, "entry-type", &entry_type, NULL);
142 rhythmdb_add_uri_with_types (db, uri, entry_type, NULL, NULL);
143 g_object_unref (entry_type);
144
145 g_object_unref (db);
146 }
147 }
148
149 /**
150 * rb_transfer_target_track_add_error:
151 * @target: an #RBTransferTarget
152 * @entry: the source #RhythmDBEntry for the transfer
153 * @uri: the destination URI
154 * @error: the transfer error information
155 *
156 * This is called when a transfer fails. If the source's
157 * impl_track_add_error implementation returns %TRUE, an error dialog
158 * will be displayed to the user containing the error message, unless
159 * the error indicates that the destination file already exists.
160 */
161 void
rb_transfer_target_track_add_error(RBTransferTarget * target,RhythmDBEntry * entry,const char * uri,GError * error)162 rb_transfer_target_track_add_error (RBTransferTarget *target,
163 RhythmDBEntry *entry,
164 const char *uri,
165 GError *error)
166 {
167 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
168 gboolean show_dialog = TRUE;
169
170 /* hrm, want the subclass to decide whether to display the error and
171 * whether to cancel the batch (may have some device-specific errors?)
172 *
173 * for now we'll just cancel on the most common things..
174 */
175 if (iface->track_add_error)
176 show_dialog = iface->track_add_error (target, entry, uri, error);
177
178 if (show_dialog) {
179 if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
180 rb_debug ("not displaying 'file exists' error for %s", uri);
181 } else {
182 rb_error_dialog (NULL, _("Error transferring track"), "%s", error->message);
183 }
184 }
185 }
186
187 /**
188 * rb_transfer_target_get_format_descriptions:
189 * @target: an #RBTransferTarget
190 *
191 * Returns a #GList of allocated media format descriptions for
192 * the formats supported by the target. The list and the strings
193 * it holds must be freed by the caller.
194 *
195 * Return value: (element-type utf8) (transfer full): list of descriptions.
196 */
197 GList *
rb_transfer_target_get_format_descriptions(RBTransferTarget * target)198 rb_transfer_target_get_format_descriptions (RBTransferTarget *target)
199 {
200 GstEncodingTarget *enctarget;
201 const GList *l;
202 GList *desc = NULL;
203 g_object_get (target, "encoding-target", &enctarget, NULL);
204 if (enctarget != NULL) {
205 for (l = gst_encoding_target_get_profiles (enctarget); l != NULL; l = l->next) {
206 GstEncodingProfile *profile = l->data;
207 desc = g_list_append (desc, g_strdup (gst_encoding_profile_get_description (profile)));
208 }
209 gst_encoding_target_unref (enctarget);
210 }
211 return desc;
212 }
213
214 /**
215 * rb_transfer_target_should_transfer:
216 * @target: an #RBTransferTarget
217 * @entry: a #RhythmDBEntry to consider transferring
218 *
219 * Checks whether @entry should be transferred to the target.
220 * The target can check whether a matching entry already exists on the device,
221 * for instance. @rb_transfer_target_check_duplicate may form part of
222 * an implementation. If this method returns %FALSE, the entry
223 * will be skipped.
224 *
225 * Return value: %TRUE if the entry should be transferred to the target
226 */
227 gboolean
rb_transfer_target_should_transfer(RBTransferTarget * target,RhythmDBEntry * entry)228 rb_transfer_target_should_transfer (RBTransferTarget *target, RhythmDBEntry *entry)
229 {
230 RBTransferTargetInterface *iface = RB_TRANSFER_TARGET_GET_IFACE (target);
231
232 return iface->should_transfer (target, entry);
233 }
234
235 /**
236 * rb_transfer_target_check_category:
237 * @target: an #RBTransferTarget
238 * @entry: a #RhythmDBEntry to check
239 *
240 * This checks that the entry type of @entry is in a suitable
241 * category for transfer. This can be used to implement
242 * @should_transfer.
243 *
244 * Return value: %TRUE if the entry is in a suitable category
245 */
246 gboolean
rb_transfer_target_check_category(RBTransferTarget * target,RhythmDBEntry * entry)247 rb_transfer_target_check_category (RBTransferTarget *target, RhythmDBEntry *entry)
248 {
249 RhythmDBEntryCategory cat;
250 RhythmDBEntryType *entry_type;
251
252 entry_type = rhythmdb_entry_get_entry_type (entry);
253 g_object_get (entry_type, "category", &cat, NULL);
254 return (cat == RHYTHMDB_ENTRY_NORMAL);
255 }
256
257 /**
258 * rb_transfer_target_check_duplicate:
259 * @target: an #RBTransferTarget
260 * @entry: a #RhythmDBEntry to check
261 *
262 * This checks for an existing entry in the target that matches
263 * the title, album, artist, and track number of the entry being
264 * considered. This can be used to implement @should_transfer.
265 *
266 * Return value: %TRUE if the entry already exists on the target.
267 */
268 gboolean
rb_transfer_target_check_duplicate(RBTransferTarget * target,RhythmDBEntry * entry)269 rb_transfer_target_check_duplicate (RBTransferTarget *target, RhythmDBEntry *entry)
270 {
271 RhythmDBEntryType *entry_type;
272 RhythmDB *db;
273 RBShell *shell;
274 const char *title;
275 const char *album;
276 const char *artist;
277 gulong track_number;
278 GtkTreeModel *query_model;
279 GtkTreeIter iter;
280 gboolean is_dup;
281
282 g_object_get (target, "shell", &shell, "entry-type", &entry_type, NULL);
283 g_object_get (shell, "db", &db, NULL);
284 g_object_unref (shell);
285
286 query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
287 title = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
288 album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
289 artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST);
290 track_number = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_TRACK_NUMBER);
291 rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
292 RHYTHMDB_QUERY_PROP_EQUALS,
293 RHYTHMDB_PROP_TYPE, entry_type,
294 RHYTHMDB_QUERY_PROP_EQUALS,
295 RHYTHMDB_PROP_ARTIST, artist,
296 RHYTHMDB_QUERY_PROP_EQUALS,
297 RHYTHMDB_PROP_ALBUM, album,
298 RHYTHMDB_QUERY_PROP_EQUALS,
299 RHYTHMDB_PROP_TITLE, title,
300 RHYTHMDB_QUERY_PROP_EQUALS,
301 RHYTHMDB_PROP_TRACK_NUMBER, track_number,
302 RHYTHMDB_QUERY_END);
303
304 is_dup = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (query_model), &iter);
305 g_object_unref (entry_type);
306 g_object_unref (query_model);
307 g_object_unref (db);
308 if (is_dup) {
309 rb_debug ("not transferring %lu - %s - %s - %s as already present",
310 track_number, title, album, artist);
311 }
312 return is_dup;
313 }
314
315 static gboolean
default_should_transfer(RBTransferTarget * target,RhythmDBEntry * entry)316 default_should_transfer (RBTransferTarget *target, RhythmDBEntry *entry)
317 {
318 if (rb_transfer_target_check_category (target, entry) == FALSE)
319 return FALSE;
320
321 return (rb_transfer_target_check_duplicate (target, entry) == FALSE);
322 }
323
324 static char *
get_dest_uri_cb(RBTrackTransferBatch * batch,RhythmDBEntry * entry,const char * mediatype,const char * extension,RBTransferTarget * target)325 get_dest_uri_cb (RBTrackTransferBatch *batch,
326 RhythmDBEntry *entry,
327 const char *mediatype,
328 const char *extension,
329 RBTransferTarget *target)
330 {
331 char *free_ext = NULL;
332 char *uri;
333
334 /* make sure the extension isn't ludicrously long */
335 if (extension == NULL) {
336 extension = "";
337 } else if (strlen (extension) > EXTENSION_LENGTH_LIMIT) {
338 free_ext = g_strdup (extension);
339 free_ext[EXTENSION_LENGTH_LIMIT] = '\0';
340 extension = free_ext;
341 }
342 uri = rb_transfer_target_build_dest_uri (target, entry, mediatype, extension);
343 g_free (free_ext);
344 return uri;
345 }
346
347 static void
track_done_cb(RBTrackTransferBatch * batch,RhythmDBEntry * entry,const char * dest,guint64 dest_size,const char * dest_mediatype,GError * error,RBTransferTarget * target)348 track_done_cb (RBTrackTransferBatch *batch,
349 RhythmDBEntry *entry,
350 const char *dest,
351 guint64 dest_size,
352 const char *dest_mediatype,
353 GError *error,
354 RBTransferTarget *target)
355 {
356 if (error != NULL) {
357 if (g_error_matches (error, RB_ENCODER_ERROR, RB_ENCODER_ERROR_OUT_OF_SPACE) ||
358 g_error_matches (error, RB_ENCODER_ERROR, RB_ENCODER_ERROR_DEST_READ_ONLY)) {
359 rb_debug ("fatal transfer error: %s", error->message);
360 rb_track_transfer_batch_cancel (batch);
361 }
362 rb_transfer_target_track_add_error (target, entry, dest, error);
363 } else {
364 rb_transfer_target_track_added (target, entry, dest, dest_size, dest_mediatype);
365 }
366 }
367
368 /**
369 * rb_transfer_target_transfer:
370 * @target: an #RBTransferTarget
371 * @settings: #GSettings instance holding encoding settings
372 * @entries: (element-type RB.RhythmDBEntry): a #GList of entries to transfer
373 * @defer: if %TRUE, don't start the transfer until
374 *
375 * Starts tranferring @entries to the target. This returns the
376 * #RBTrackTransferBatch that it starts, so the caller can track
377 * the progress of the transfer, or NULL if the target doesn't
378 * want any of the entries.
379 *
380 * Return value: (transfer full): an #RBTrackTransferBatch, or NULL
381 */
382 RBTrackTransferBatch *
rb_transfer_target_transfer(RBTransferTarget * target,GSettings * settings,GList * entries,gboolean defer)383 rb_transfer_target_transfer (RBTransferTarget *target, GSettings *settings, GList *entries, gboolean defer)
384 {
385 RBTrackTransferQueue *xferq;
386 RBTaskList *tasklist;
387 RBShell *shell;
388 GList *l;
389 RhythmDBEntryType *our_entry_type;
390 RBTrackTransferBatch *batch;
391 gboolean start_batch = FALSE;
392
393 g_object_get (target,
394 "shell", &shell,
395 "entry-type", &our_entry_type,
396 NULL);
397 g_object_get (shell,
398 "track-transfer-queue", &xferq,
399 "task-list", &tasklist,
400 NULL);
401 g_object_unref (shell);
402
403 batch = g_object_steal_data (G_OBJECT (target), "transfer-target-batch");
404
405 if (batch == NULL) {
406 batch = rb_track_transfer_batch_new (NULL, settings, NULL, G_OBJECT (target), G_OBJECT (xferq));
407
408 g_signal_connect_object (batch, "get-dest-uri", G_CALLBACK (get_dest_uri_cb), target, 0);
409 g_signal_connect_object (batch, "track-done", G_CALLBACK (track_done_cb), target, 0);
410 } else {
411 start_batch = TRUE;
412 }
413
414 for (l = entries; l != NULL; l = l->next) {
415 RhythmDBEntry *entry;
416 RhythmDBEntryType *entry_type;
417 const char *location;
418
419 entry = (RhythmDBEntry *)l->data;
420 location = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
421 entry_type = rhythmdb_entry_get_entry_type (entry);
422
423 if (entry_type != our_entry_type) {
424 if (rb_transfer_target_should_transfer (target, entry)) {
425 rb_debug ("pasting entry %s", location);
426 rb_track_transfer_batch_add (batch, entry);
427 start_batch = TRUE;
428 } else {
429 rb_debug ("target doesn't want entry %s", location);
430 }
431 } else {
432 rb_debug ("can't copy entry %s from the target to itself", location);
433 }
434 }
435 g_object_unref (our_entry_type);
436
437 if (start_batch) {
438 if (defer) {
439 g_object_set_data_full (G_OBJECT (target), "transfer-target-batch", g_object_ref (batch), g_object_unref);
440 } else {
441 GstEncodingTarget *encoding_target;
442 char *name;
443 char *label;
444
445 g_object_get (target, "encoding-target", &encoding_target, NULL);
446 g_object_set (batch, "encoding-target", encoding_target, NULL);
447 gst_encoding_target_unref (encoding_target);
448
449 g_object_get (target, "name", &name, NULL);
450 label = g_strdup_printf (_("Transferring tracks to %s"), name);
451 g_object_set (batch, "task-label", label, NULL);
452 g_free (name);
453 g_free (label);
454
455 rb_task_list_add_task (tasklist, RB_TASK_PROGRESS (batch));
456
457 rb_track_transfer_queue_start_batch (xferq, batch);
458 }
459 } else {
460 g_object_unref (batch);
461 batch = NULL;
462 }
463 g_object_unref (xferq);
464 g_object_unref (tasklist);
465 return batch;
466 }
467
468
469 static void
rb_transfer_target_default_init(RBTransferTargetInterface * interface)470 rb_transfer_target_default_init (RBTransferTargetInterface *interface)
471 {
472 interface->should_transfer = default_should_transfer;
473
474 g_object_interface_install_property (interface,
475 g_param_spec_object ("encoding-target",
476 "encoding target",
477 "GstEncodingTarget",
478 GST_TYPE_ENCODING_TARGET,
479 G_PARAM_READWRITE));
480 }
481