Merge lp:~artem-anufrij/audience/upnp-client into lp:~audience-members/audience/trunk

Proposed by Artem Anufrij
Status: Needs review
Proposed branch: lp:~artem-anufrij/audience/upnp-client
Merge into: lp:~audience-members/audience/trunk
Diff against target: 809 lines (+416/-90)
9 files modified
src/CMakeLists.txt (+9/-0)
src/Objects/Video.vala (+113/-57)
src/Services/LibraryManager.vala (+54/-11)
src/Services/UPnPManager.vala (+173/-0)
src/Widgets/Library/EpisodesPage.vala (+10/-4)
src/Widgets/Library/LibraryItem.vala (+20/-10)
src/Widgets/Library/LibraryPage.vala (+29/-7)
src/Widgets/WelcomePage.vala (+7/-0)
src/Window.vala (+1/-1)
To merge this branch: bzr merge lp:~artem-anufrij/audience/upnp-client
Reviewer Review Type Date Requested Status
Zisu Andrei (community) Needs Fixing
Review via email: mp+310490@code.launchpad.net

Description of the change

UPnP-client support

To post a comment you must log in.
Revision history for this message
Zisu Andrei (matzipan) wrote :

AFAICT looks good. Some review inline.

review: Needs Fixing
699. By Artem Anufrij

support Rygel connection

700. By Artem Anufrij

show welcomescreen button when UPnP items detected

Revision history for this message
Danielle Foré (danrabbit) wrote :

Audience has migrated to GitHub. Please resubmit as a PR here: https://github.com/elementary/videos

Unmerged revisions

700. By Artem Anufrij

show welcomescreen button when UPnP items detected

699. By Artem Anufrij

support Rygel connection

698. By Artem Anufrij

fixed poster size for plex items

697. By Artem Anufrij

plex support can be tested

696. By Artem Anufrij

switch from SEARCH to BROWSE --> Plex doesn't support SEARCH action

695. By Artem Anufrij

remove items from library if device is no more available

694. By Artem Anufrij

added files

693. By Artem Anufrij

video-x-generic-symbolic for upnp items without poster

692. By Artem Anufrij

development

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/CMakeLists.txt'
2--- src/CMakeLists.txt 2016-11-02 23:59:41 +0000
3+++ src/CMakeLists.txt 2016-11-12 11:33:11 +0000
4@@ -19,6 +19,10 @@
5 gstreamer-video-1.0
6 gstreamer-tag-1.0
7 clutter-gst-3.0
8+ gupnp-1.0
9+ gupnp-av-1.0
10+ gssdp-1.0
11+ libxml-2.0
12 )
13
14 set (VALA_DEPS
15@@ -30,6 +34,10 @@
16 gstreamer-video-1.0
17 gstreamer-tag-1.0
18 clutter-gst-3.0
19+ gupnp-1.0
20+ gupnp-av-1.0
21+ gssdp-1.0
22+ libxml-2.0
23 )
24
25 pkg_check_modules (DEPS REQUIRED ${PKG_DEPS})
26@@ -69,6 +77,7 @@
27 Services/Inhibitor.vala
28 Services/DirictoryMonitoring.vala
29 Services/LibraryManager.vala
30+ Services/UPnPManager.vala
31 Services/Thubnailer.vala
32
33 Objects/Video.vala
34
35=== modified file 'src/Objects/Video.vala'
36--- src/Objects/Video.vala 2016-09-27 20:26:37 +0000
37+++ src/Objects/Video.vala 2016-11-12 11:33:11 +0000
38@@ -1,4 +1,4 @@
39-/*-
40+ /*-
41 * Copyright (c) 2016-2016 elementary LLC.
42 *
43 * This program is free software: you can redistribute it and/or modify
44@@ -19,6 +19,9 @@
45 */
46
47 namespace Audience.Objects {
48+
49+ public enum LibraryVideoType { LOCAL, UPNP }
50+
51 public class Video : Object {
52 Audience.Services.LibraryManager manager;
53
54@@ -27,18 +30,12 @@
55 public signal void thumbnail_changed ();
56 public signal void trashed ();
57
58- public File video_file { get; private set; }
59- public string directory { get; construct set; }
60- public string file { get; construct set; }
61-
62- public string title { get; private set; }
63+ public string title { get; construct set; }
64 public int year { get; private set; default = -1;}
65
66 public Gdk.Pixbuf? poster { get; private set; default = null; }
67- public Gdk.Pixbuf? thumbnail { get; private set; default = null; }
68
69 public string mime_type { get; construct set; }
70- public string poster_cache_file { get; private set; }
71
72 public string hash_file_poster { get; construct set; }
73 public string hash_episode_poster { get; construct set; }
74@@ -47,17 +44,33 @@
75
76 public string container { get; construct set; default = ""; }
77
78- public Video (string directory, string file, string mime_type) {
79- Object (directory: directory, file: file, mime_type: mime_type);
80- }
81-
82- construct {
83- manager = Audience.Services.LibraryManager.get_instance ();
84- manager.thumbler.finished.connect (dbus_finished);
85-
86- title = Audience.get_title (file);
87-
88- extract_metadata ();
89+ // Local
90+ public File video_file { get; private set; }
91+ public string directory { get; construct set; }
92+ public string file { get; construct set; }
93+ public Gdk.Pixbuf? thumbnail { get; private set; default = null; }
94+ public string poster_cache_file { get; private set; }
95+
96+ // UPnP
97+ public string video_uri { get; construct set; default = "";}
98+ public string poster_uri { get; construct set; }
99+
100+
101+ public LibraryVideoType library_video_type { get; private set; }
102+
103+ public Video.local_media (string directory, string file, string mime_type) {
104+ Object (title: Audience.get_title (file), directory: directory, file: file, mime_type: mime_type);
105+ init_local ();
106+ }
107+
108+ public Video.upnp_media (string container, string title, string video_uri, string poster_uri, string mime_type) {
109+ Object (container: container, title: title, video_uri: video_uri, poster_uri: poster_uri, mime_type: mime_type);
110+ init_upnp ();
111+ }
112+
113+ private void init_local () {
114+ library_video_type = LibraryVideoType.LOCAL;
115+
116 video_file = File.new_for_path (this.get_path ());
117
118 if (directory != Audience.settings.library_folder) {
119@@ -70,6 +83,20 @@
120 thumbnail_large_path = Path.build_filename (GLib.Environment.get_user_cache_dir (),"thumbnails", "large", hash_file_poster + ".png");
121 thumbnail_normal_path = Path.build_filename (GLib.Environment.get_user_cache_dir (),"thumbnails", "normal", hash_file_poster + ".png");
122 poster_cache_file = Path.build_filename (App.get_instance ().get_cache_directory (), hash_file_poster + ".jpg");
123+ }
124+
125+ private void init_upnp () {
126+ library_video_type = LibraryVideoType.UPNP;
127+ video_file = File.new_for_uri (video_uri);
128+
129+ hash_file_poster = GLib.Checksum.compute_for_string (ChecksumType.MD5, video_file.get_uri (), video_file.get_uri ().length);
130+ thumbnail_large_path = Path.build_filename (GLib.Environment.get_user_cache_dir (),"thumbnails", "large", hash_file_poster + ".png");
131+ thumbnail_normal_path = Path.build_filename (GLib.Environment.get_user_cache_dir (),"thumbnails", "normal", hash_file_poster + ".png");
132+ }
133+
134+ construct {
135+ manager = Audience.Services.LibraryManager.get_instance ();
136+ manager.thumbler.finished.connect (dbus_finished);
137
138 notify["poster"].connect (() => {
139 poster_changed (this);
140@@ -80,6 +107,8 @@
141 notify["thumbnail"].connect (() => {
142 thumbnail_changed ();
143 });
144+
145+ extract_metadata ();
146 }
147
148 private void extract_metadata () {
149@@ -87,8 +116,9 @@
150 MatchInfo info;
151 if (manager.regex_year.match (this.title, 0, out info)) {
152 year = int.parse (info.fetch (0).substring (1, 4));
153- title = this.title.replace (info.fetch (0) + ")", "").strip ();
154+ title = title.replace (info.fetch (0) + ")", "").replace ("_", " ").strip ();
155 }
156+ title = title.replace ("_", " ").strip ();
157 }
158
159 public async void initialize_poster () {
160@@ -103,47 +133,73 @@
161 Gdk.Pixbuf? pixbuf = null;
162
163 ThreadFunc<void*> run = () => {
164- if (!FileUtils.test (thumbnail_large_path, FileTest.EXISTS) || !FileUtils.test (thumbnail_normal_path, FileTest.EXISTS)) {
165- // Call DBUS for create a new THUMBNAIL
166- Gee.ArrayList<string> uris = new Gee.ArrayList<string> ();
167- Gee.ArrayList<string> mimes = new Gee.ArrayList<string> ();
168-
169- uris.add (video_file.get_uri ());
170- mimes.add (mime_type);
171-
172- manager.thumbler.Instand (uris, mimes, "large");
173- manager.thumbler.Instand (uris, mimes, "normal");
174- }
175-
176- string? poster_path = poster_cache_file;
177- pixbuf = manager.get_poster_from_file (poster_path);
178-
179- // POSTER in Cache exists
180- if (pixbuf != null) {
181- Idle.add ((owned) callback);
182- return null;
183- }
184-
185- poster_path = get_native_poster_path ();
186- if (poster_path != null) {
187- pixbuf = manager.get_poster_from_file (poster_path);
188- }
189-
190- // POSTER found
191- if (pixbuf != null) {
192+ if (library_video_type == LibraryVideoType.LOCAL) {
193+ if (!FileUtils.test (thumbnail_large_path, FileTest.EXISTS) || !FileUtils.test (thumbnail_normal_path, FileTest.EXISTS)) {
194+ // Call DBUS for create a new THUMBNAIL
195+ Gee.ArrayList<string> uris = new Gee.ArrayList<string> ();
196+ Gee.ArrayList<string> mimes = new Gee.ArrayList<string> ();
197+ uris.add (video_file.get_uri ());
198+ mimes.add (mime_type);
199+
200+ manager.thumbler.Instand (uris, mimes, "large");
201+ manager.thumbler.Instand (uris, mimes, "normal");
202+ }
203+
204+ string? poster_path = poster_cache_file;
205+ pixbuf = manager.get_poster_from_file (poster_path);
206+
207+ // POSTER in Cache exists
208+ if (pixbuf != null) {
209+ Idle.add ((owned) callback);
210+ return null;
211+ }
212+
213+ poster_path = get_native_poster_path ();
214+ if (poster_path != null) {
215+ pixbuf = manager.get_poster_from_file (poster_path);
216+ }
217+
218+ // POSTER found
219+ if (pixbuf != null) {
220+ try {
221+ pixbuf.save (poster_cache_file, "jpeg");
222+ } catch (Error e) {
223+ warning (e.message);
224+ }
225+ Idle.add ((owned) callback);
226+ return null;
227+ }
228+
229+ if (FileUtils.test (thumbnail_large_path, FileTest.EXISTS)) {
230+ pixbuf = manager.get_poster_from_file (thumbnail_large_path);
231+ Idle.add ((owned) callback);
232+ return null;
233+ }
234+ } else if (library_video_type == LibraryVideoType.UPNP) {
235+ string? poster_path = poster_uri;
236+ pixbuf = manager.get_poster_from_file (poster_path);
237+
238+ // UPnP POSTER exists
239+ if (pixbuf != null) {
240+ Idle.add ((owned) callback);
241+ return null;
242+ }
243+ var p = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, Audience.Services.POSTER_WIDTH, Audience.Services.POSTER_HEIGHT);
244 try {
245- pixbuf.save (poster_cache_file, "jpeg");
246+ pixbuf = Gtk.IconTheme.get_default().load_icon ("video-x-generic-symbolic", 64, Gtk.IconLookupFlags.USE_BUILTIN);
247 } catch (Error e) {
248 warning (e.message);
249 }
250- Idle.add ((owned) callback);
251- return null;
252- }
253-
254- if (FileUtils.test (thumbnail_large_path, FileTest.EXISTS)) {
255- pixbuf = manager.get_poster_from_file (thumbnail_large_path);
256- Idle.add ((owned) callback);
257- return null;
258+ pixbuf.scale (p, 0, 0, Audience.Services.POSTER_WIDTH, Audience.Services.POSTER_HEIGHT,
259+ (Audience.Services.POSTER_WIDTH - 64) / 2,
260+ (Audience.Services.POSTER_HEIGHT - 64) / 2 - 1, 1, 1, Gdk.InterpType.HYPER);
261+
262+ pixbuf = p;
263+
264+ if (pixbuf != null) {
265+ Idle.add ((owned) callback);
266+ return null;
267+ }
268 }
269
270 Idle.add ((owned) callback);
271
272=== modified file 'src/Services/LibraryManager.vala'
273--- src/Services/LibraryManager.vala 2016-09-27 20:26:37 +0000
274+++ src/Services/LibraryManager.vala 2016-11-12 11:33:11 +0000
275@@ -129,7 +129,7 @@
276 if (name == "") {
277 name = file_info.get_name ();
278 }
279- var video = new Audience.Objects.Video (source, name, file_info.get_content_type ());
280+ var video = new Audience.Objects.Video.local_media (source, name, file_info.get_content_type ());
281 video_file_detected (video);
282 poster_hash.add (video.hash_file_poster + ".jpg");
283 poster_hash.add (video.hash_episode_poster + ".jpg");
284@@ -194,22 +194,65 @@
285
286 public Gdk.Pixbuf? get_poster_from_file (string poster_path) {
287 Gdk.Pixbuf? pixbuf = null;
288- if (FileUtils.test (poster_path, FileTest.EXISTS)) {
289- try {
290- pixbuf = new Gdk.Pixbuf.from_file_at_scale (poster_path, -1, Audience.Services.POSTER_HEIGHT, true);
291- } catch (Error e) {
292- warning (e.message);
293- }
294- if (pixbuf == null) {
295- return null;
296- }
297- // Cut THUMBNAIL images
298+
299+ if (poster_path.has_prefix ("http")) {
300+ var http_file = File.new_for_uri (poster_path);
301+ FileInputStream http_input;
302+ try {
303+ http_input = http_file.read ();
304+ } catch (Error e) {
305+ warning ("Unable to fetch '%s': %s", poster_path, e.message);
306+ return null;
307+ }
308+
309+ var loader = new Gdk.PixbufLoader ();
310+ var buf = new uint8[4096];
311+ for (;;) {
312+ ssize_t bytes_read;
313+ try {
314+ bytes_read = http_input.read (buf);
315+ if (bytes_read == 0)
316+ break;
317+ } catch (IOError e) {
318+ warning ("Unable to read data from '%s': %s", poster_path, e.message);
319+ return null;
320+ }
321+ try {
322+ loader.write (buf[0:bytes_read]);
323+ } catch (Error e) {
324+ warning ("Unable to parse image from '%s': %s", poster_path, e.message);
325+ return null;
326+ }
327+ }
328+ try {
329+ loader.close ();
330+ } catch (Error e) {
331+ warning ("Unable to parse image from '%s': %s", poster_path, e.message);
332+ return null;
333+ }
334+ pixbuf = loader.get_pixbuf ();
335+ } else {
336+ if (poster_path != "" && FileUtils.test (poster_path, FileTest.EXISTS)) {
337+ try {
338+ pixbuf = new Gdk.Pixbuf.from_file_at_scale (poster_path, -1, Audience.Services.POSTER_HEIGHT, true);
339+ } catch (Error e) {
340+ warning (e.message);
341+ }
342+ if (pixbuf == null) {
343+ return null;
344+ }
345+ }
346+ }
347+
348+ // Cut THUMBNAIL images
349+ if (pixbuf != null) {
350 int width = pixbuf.width;
351 if (width > Audience.Services.POSTER_WIDTH) {
352 int x_offset = (width - Audience.Services.POSTER_WIDTH) / 2;
353 pixbuf = new Gdk.Pixbuf.subpixbuf (pixbuf, x_offset, 0, Audience.Services.POSTER_WIDTH, Audience.Services.POSTER_HEIGHT);
354 }
355 }
356+
357 return pixbuf;
358 }
359 }
360
361=== added file 'src/Services/UPnPManager.vala'
362--- src/Services/UPnPManager.vala 1970-01-01 00:00:00 +0000
363+++ src/Services/UPnPManager.vala 2016-11-12 11:33:11 +0000
364@@ -0,0 +1,173 @@
365+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
366+/*-
367+ * Copyright (c) 2016-2016 elementary LLC.
368+ *
369+ * This program is free software: you can redistribute it and/or modify
370+ * it under the terms of the GNU General Public License as published by
371+ * the Free Software Foundation, either version 3 of the License, or
372+ * (at your option) any later version.
373+
374+ * This program is distributed in the hope that it will be useful,
375+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
376+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
377+ * GNU General Public License for more details.
378+
379+ * You should have received a copy of the GNU General Public License
380+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
381+ *
382+ * Authored by: Artem Anufrij <artem.anufrij@live.de>
383+ *
384+ */
385+
386+namespace Audience.Services {
387+ public class UPnPManager : Object {
388+
389+ public signal void video_item_detected (Audience.Objects.Video video);
390+ public signal void sevice_unavailable (string device_uri);
391+
392+ GUPnP.Context context;
393+ GUPnP.ControlPoint point;
394+
395+ public static UPnPManager instance = null;
396+ public static UPnPManager get_instance () {
397+ if (instance == null) {
398+ instance = new UPnPManager ();
399+ }
400+
401+ return instance;
402+ }
403+
404+ private UPnPManager () {}
405+
406+ construct {
407+ context = new GUPnP.Context(null, null, 0);
408+
409+ point = new GUPnP.ControlPoint(context, "urn:schemas-upnp-org:device:MediaServer:1");
410+ point.device_proxy_unavailable.connect ((device_proxy) => {
411+ debug ("DEVICE OFF: %s", device_proxy.get_friendly_name ());
412+ string device_uri = device_proxy.get_presentation_url ();
413+ sevice_unavailable (device_uri);
414+ });
415+ point.device_proxy_available.connect ((device_proxy) => {
416+ debug ("DEVICE ON: %s", device_proxy.get_friendly_name ());
417+ foreach (var si in device_proxy.list_services ()) {
418+ if (si.service_type.has_prefix("urn:schemas-upnp-org:service:ContentDirectory") && si is GUPnP.ServiceProxy) {
419+ var proxy = si as GUPnP.ServiceProxy;
420+ try {
421+ Gee.ArrayList<string> items = new Gee.ArrayList<string> ();
422+ expand_content (proxy, "0", items, "root");
423+ } catch (Error e) {
424+ warning (e.message);
425+ }
426+ }
427+ }
428+ debug ("DEVICE SCAN FINISHED: %s", device_proxy.get_friendly_name ());
429+ });
430+ }
431+
432+ private void expand_content (GUPnP.ServiceProxy proxy, string object_id, Gee.ArrayList<string> items, string parent) {
433+ // SEARCH PARAMETER IN
434+ List<string> in_names = new List<string> ();
435+ List<unowned Value?> in_values = new List<unowned Value?> ();
436+ in_names.append ("ObjectID");
437+ in_names.append ("BrowseFlag");
438+ in_names.append ("StartingIndex");
439+ in_names.append ("RequestedCount");
440+ in_names.append ("Filter");
441+ in_names.append ("SortCriteria");
442+ var val_object_id = Value (typeof (string));
443+ val_object_id.set_string (object_id);
444+ var val_browse_crit = Value (typeof (string));
445+ val_browse_crit.set_string ("BrowseDirectChildren");
446+ var val_start_index = Value (typeof (int));
447+ val_start_index.set_int (0);
448+ var val_req_count = Value (typeof (int));
449+ val_req_count.set_int (0);
450+ var val_filter = Value (typeof (string));
451+ val_filter.set_string ("*");
452+ var val_sort_crit = Value (typeof (string));
453+ val_sort_crit.set_string ("");
454+ in_values.append (val_object_id);
455+ in_values.append (val_browse_crit);
456+ in_values.append (val_start_index);
457+ in_values.append (val_req_count);
458+ in_values.append (val_filter);
459+ in_values.append (val_sort_crit);
460+
461+ // SEARCH PARAMETER OUT
462+ List<string> out_names = new List<string> ();
463+ List<Type> out_types = new List<Type> ();
464+ List<Value*> out_values = new List<Value*> ();
465+ out_names.append ("Result");
466+ out_types.append (typeof (string));
467+
468+ proxy.send_action_list ("Browse", in_names, in_values, out_names, out_types, out out_values);
469+
470+ var parser = new GUPnP.DIDLLiteParser ();
471+ parser.container_available.connect ((container) => {
472+ if (container.id != object_id && (container.parent_id == object_id || object_id == "0")) {
473+ expand_content (proxy, container.id, items, container.title);
474+ }
475+ });
476+ parser.item_available.connect ((item) => {
477+ crate_upnp_object (item, items, parent);
478+ });
479+
480+ // PARSE ITEMS
481+ foreach (Value* v in out_values) {
482+ string output = v.get_string();
483+ v.unset();
484+ GLib.Slice.free (sizeof(Value), v);
485+ try {
486+ GUPnP.MediaCollection media_collection = new GUPnP.MediaCollection.from_string (output);
487+ if (media_collection.get_items ().length () > 0) {
488+ foreach (var item in media_collection.get_items ()) {
489+ crate_upnp_object (item, items, parent);
490+ }
491+ } else {
492+ parser.parse_didl (output);
493+ }
494+
495+ } catch (Error e) {
496+ //PARSTER THROWS AN EXCEPTION IF VALUE IS EMPTY
497+ //IT CAN BE IGNORED
498+ debug (e.message);
499+ }
500+ }
501+ }
502+
503+ private void crate_upnp_object (GUPnP.DIDLLiteObject item, Gee.ArrayList<string> items, string parent) {
504+ if (item.upnp_class.has_prefix("object.item.videoItem")) {
505+ string video_uri = "";
506+ string poster_uri = "";
507+ string mime_type = "";
508+
509+ foreach (var res in item.get_resources ()) {
510+ if (res.protocol_info.mime_type.has_prefix ("video")) {
511+ video_uri = res.uri;
512+ mime_type = res.protocol_info.mime_type;
513+ } else if (res.protocol_info.mime_type.has_prefix ("image")) {
514+ poster_uri = res.uri;
515+ }
516+ }
517+
518+ if (poster_uri == "" && item.album_art != null) {
519+ poster_uri = item.album_art;
520+ }
521+
522+ if (video_uri != "" && !items.contains (video_uri)) {
523+ items.add (video_uri);
524+ video_item_detected (new Audience.Objects.Video.upnp_media (parent, item.title, video_uri, poster_uri, mime_type));
525+ }
526+ }
527+ }
528+
529+ public bool active {
530+ get {
531+ return point.active;
532+ } set {
533+ point.active = value;
534+ }
535+ }
536+ }
537+}
538
539=== modified file 'src/Widgets/Library/EpisodesPage.vala'
540--- src/Widgets/Library/EpisodesPage.vala 2016-11-02 23:59:41 +0000
541+++ src/Widgets/Library/EpisodesPage.vala 2016-11-12 11:33:11 +0000
542@@ -27,6 +27,7 @@
543 Granite.Widgets.AlertView alert_view;
544
545 public Audience.Services.LibraryManager manager;
546+ public Audience.Services.UPnPManager upnp;
547
548 string query;
549
550@@ -65,6 +66,9 @@
551 manager = Audience.Services.LibraryManager.get_instance ();
552 manager.video_file_deleted.connect (remove_item_from_path);
553 manager.video_file_detected.connect (add_item);
554+
555+ upnp = Audience.Services.UPnPManager.get_instance ();
556+ upnp.sevice_unavailable.connect (remove_item_from_path);
557 }
558
559 public void set_episodes_items (Gee.ArrayList<Audience.Objects.Video> episodes) {
560@@ -80,6 +84,7 @@
561 private void play_video (Gtk.FlowBoxChild item) {
562 var selected = (item as Audience.LibraryItem);
563 var video = selected.episodes.first ();
564+ debug (video.video_file.get_uri () + " - " + video.video_file.query_exists ().to_string ());
565 if (video.video_file.query_exists ()) {
566 bool from_beginning = video.video_file.get_uri () != settings.current_video;
567 App.get_instance ().mainwindow.play_file (video.video_file.get_uri (), from_beginning);
568@@ -102,10 +107,9 @@
569 }
570
571 string[] filter_elements = query.split (" ");
572- var video_title = (child as LibraryItem).get_title ();
573
574 foreach (string filter_element in filter_elements) {
575- if (!video_title.down ().contains (filter_element.down ())) {
576+ if (!(child as LibraryItem).title.down ().contains (filter_element.down ())) {
577 return false;
578 }
579 }
580@@ -116,7 +120,7 @@
581 var item1 = (LibraryItem)child1;
582 var item2 = (LibraryItem)child2;
583 if (item1 != null && item2 != null) {
584- return item1.episodes.first ().file.collate (item2.episodes.first ().file);
585+ return item1.episodes.first ().title.collate (item2.episodes.first ().title);
586 }
587 return 0;
588 }
589@@ -132,7 +136,9 @@
590
591 private async void remove_item_from_path (string path ) {
592 foreach (var child in view_episodes.get_children ()) {
593- if ((child as LibraryItem).episodes.size == 0 || (child as LibraryItem).episodes.first ().video_file.get_path ().has_prefix (path)) {
594+ if ((child as LibraryItem).episodes.size == 0
595+ || (child as LibraryItem).episodes.first ().video_file.get_uri ().has_prefix (path)
596+ || (child as LibraryItem).episodes.first ().video_file.get_path ().has_prefix (path)) {
597 child.dispose ();
598 }
599 }
600
601=== modified file 'src/Widgets/Library/LibraryItem.vala'
602--- src/Widgets/Library/LibraryItem.vala 2016-11-02 23:59:41 +0000
603+++ src/Widgets/Library/LibraryItem.vala 2016-11-12 11:33:11 +0000
604@@ -50,9 +50,13 @@
605 video.title_changed.connect (video_title_changed);
606 video.poster_changed.connect (video_poster_changed);
607
608- hash = GLib.Checksum.compute_for_string (ChecksumType.MD5, video.video_file.get_parent ().get_uri (), video.video_file.get_parent ().get_uri ().length);
609- episode_poster_path = Path.build_filename (video.video_file.get_parent ().get_path (), video.video_file.get_parent ().get_basename () + ".jpg");
610- poster_cache_file = Path.build_filename (App.get_instance ().get_cache_directory (), hash + ".jpg");
611+ if (video.library_video_type == Audience.Objects.LibraryVideoType.LOCAL) {
612+ hash = GLib.Checksum.compute_for_string (ChecksumType.MD5, video.video_file.get_parent ().get_uri (), video.video_file.get_parent ().get_uri ().length);
613+ episode_poster_path = Path.build_filename (video.video_file.get_parent ().get_path (), video.video_file.get_parent ().get_basename () + ".jpg");
614+ poster_cache_file = Path.build_filename (App.get_instance ().get_cache_directory (), hash + ".jpg");
615+ } else if (video.library_video_type == Audience.Objects.LibraryVideoType.UPNP) {
616+ poster_cache_file = video.poster_uri;
617+ }
618 }
619
620 construct {
621@@ -141,7 +145,9 @@
622 }
623
624 private bool show_context_menu (Gtk.Widget sender, Gdk.EventButton evt) {
625- if (evt.type == Gdk.EventType.BUTTON_PRESS && evt.button == 3) {
626+ if (episodes.first ().library_video_type == Audience.Objects.LibraryVideoType.LOCAL
627+ && evt.type == Gdk.EventType.BUTTON_PRESS
628+ && evt.button == 3) {
629 context_menu.popup (null, null, null, evt.button, evt.time);
630 return true;
631 }
632@@ -156,9 +162,9 @@
633
634 private void video_title_changed (Audience.Objects.Video video) {
635 if (episodes.size == 1) {
636- title_label.label = video.title;
637+ title = video.title;
638 } else {
639- title_label.label = video.container;
640+ title = video.container;
641 }
642 title_label.show ();
643 }
644@@ -220,15 +226,19 @@
645 });
646 episodes.add (episode);
647 if (episodes.size == 1) {
648- title_label.label = episode.title;
649+ title = episode.title;
650 } else if (episodes.size == 2) {
651- title_label.label = episode.container;
652+ title = episode.container;
653 create_episode_poster ();
654 }
655 }
656
657- public string get_title () {
658- return title_label.label;
659+ public string title {
660+ get {
661+ return title_label.label;
662+ } set {
663+ title_label.label = value;
664+ }
665 }
666
667 public void create_episode_poster () {
668
669=== modified file 'src/Widgets/Library/LibraryPage.vala'
670--- src/Widgets/Library/LibraryPage.vala 2016-11-02 23:59:41 +0000
671+++ src/Widgets/Library/LibraryPage.vala 2016-11-12 11:33:11 +0000
672@@ -27,6 +27,7 @@
673
674 public Gtk.FlowBox view_movies;
675 public Audience.Services.LibraryManager manager;
676+ public Audience.Services.UPnPManager upnp;
677 public Gtk.ScrolledWindow scrolled_window;
678 bool poster_initialized = false;
679 string query;
680@@ -45,7 +46,7 @@
681
682 construct {
683 manager = Audience.Services.LibraryManager.get_instance ();
684-
685+ upnp = Audience.Services.UPnPManager.get_instance ();
686 query = "";
687
688 scrolled_window = new Gtk.ScrolledWindow (null, null);
689@@ -62,7 +63,16 @@
690
691 scrolled_window.add (view_movies);
692
693- manager = Audience.Services.LibraryManager.get_instance ();
694+ upnp.sevice_unavailable.connect ((device_uri) => {
695+ foreach (var child in view_movies.get_children ()) {
696+ foreach (var video in (child as LibraryItem).episodes) {
697+ if (video.video_uri != null && video.video_uri.has_prefix(device_uri)) {
698+ remove_item.begin (child as LibraryItem);
699+ }
700+ }
701+ }
702+ });
703+
704 manager.video_file_detected.connect (add_item);
705 manager.video_file_deleted.connect (remove_item_from_path);
706 manager.video_moved_to_trash.connect ((video) => {
707@@ -71,6 +81,10 @@
708
709 manager.begin_scan ();
710
711+ upnp.video_item_detected.connect (add_item);
712+ upnp.active = true;
713+
714+
715 map.connect (() => {
716 if (!poster_initialized) {
717 poster_initialized = true;
718@@ -89,7 +103,11 @@
719
720 if (selected.episodes.size == 1) {
721 bool from_beginning = selected.episodes.first ().video_file.get_uri () != settings.current_video;
722- App.get_instance ().mainwindow.play_file (selected.episodes.first ().video_file.get_uri (), from_beginning);
723+ if (selected.episodes.first ().library_video_type == Audience.Objects.LibraryVideoType.LOCAL) {
724+ App.get_instance ().mainwindow.play_file (selected.episodes.first ().video_file.get_uri (), from_beginning);
725+ } else if (selected.episodes.first ().library_video_type == Audience.Objects.LibraryVideoType.UPNP) {
726+ App.get_instance ().mainwindow.play_file (selected.episodes.first ().video_uri, from_beginning);
727+ }
728 } else {
729 last_filter = query;
730 show_episodes (selected);
731@@ -100,6 +118,9 @@
732 foreach (var child in view_movies.get_children ()) {
733 if (video.container != null && (child as LibraryItem).episodes.first ().container == video.container) {
734 (child as LibraryItem).add_episode (video);
735+ if ((child as LibraryItem).episodes.size == 2) {
736+ view_movies.invalidate_sort ();
737+ }
738 return;
739 }
740 }
741@@ -113,7 +134,9 @@
742
743 private async void remove_item (LibraryItem item) {
744 foreach (var video in item.episodes) {
745- manager.clear_cache.begin (video.poster_cache_file);
746+ if (video.poster_cache_file != null) {
747+ manager.clear_cache.begin (video.poster_cache_file);
748+ }
749 }
750 item.dispose ();
751 }
752@@ -142,10 +165,9 @@
753 }
754
755 string[] filter_elements = query.split (" ");
756- var video_title = (child as LibraryItem).get_title ();
757
758 foreach (string filter_element in filter_elements) {
759- if (!video_title.down ().contains (filter_element.down ())) {
760+ if (!(child as LibraryItem).title.down ().contains (filter_element.down ())) {
761 return false;
762 }
763 }
764@@ -156,7 +178,7 @@
765 var item1 = (LibraryItem)child1;
766 var item2 = (LibraryItem)child2;
767 if (item1 != null && item2 != null) {
768- return item1.get_title ().collate (item2.get_title ());
769+ return item1.title.collate (item2.title);
770 }
771 return 0;
772 }
773
774=== modified file 'src/Widgets/WelcomePage.vala'
775--- src/Widgets/WelcomePage.vala 2016-09-28 02:32:33 +0000
776+++ src/Widgets/WelcomePage.vala 2016-11-12 11:33:11 +0000
777@@ -2,6 +2,7 @@
778 public class WelcomePage : Granite.Widgets.Welcome {
779 private DiskManager disk_manager;
780 private Services.LibraryManager library_manager;
781+ private Services.UPnPManager upnp_manager;
782 public WelcomePage () {
783 base (_("No Videos Open"), _("Select a source to begin playing."));
784 }
785@@ -44,6 +45,12 @@
786 set_item_visible (3, LibraryPage.get_instance ().has_items);
787 });
788
789+ upnp_manager = Services.UPnPManager.get_instance ();
790+ upnp_manager.video_item_detected.connect ((vid) => {
791+ set_item_visible (3, true);
792+ this.show_all ();
793+ });
794+
795 append ("media-cdrom", _("Play from Disc"), _("Watch a DVD or open a file from disc"));
796 set_item_visible (2, disk_manager.has_media_volumes ());
797
798
799=== modified file 'src/Window.vala'
800--- src/Window.vala 2016-09-27 19:58:34 +0000
801+++ src/Window.vala 2016-11-12 11:33:11 +0000
802@@ -106,7 +106,7 @@
803 episodes_page.set_episodes_items (item.episodes);
804 episodes_page.poster.pixbuf = item.poster.pixbuf;
805 main_stack.set_visible_child (episodes_page);
806- this.title = item.get_title ();
807+ this.title = item.title;
808 search_entry.text = "";
809 });
810

Subscribers

People subscribed via source and target branches