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