├── .gitignore ├── LICENSE ├── Makefile.am ├── README.md ├── TODO.md ├── autogen.sh ├── common ├── Application.js ├── BookmarkView.js ├── ChannelView.js ├── HistoryView.js ├── ItemView.js ├── Layouts.js ├── Logger.js ├── MainWindow.js ├── Makefile.am ├── Player.js ├── PlayerEngine.js ├── Preferences.js ├── ResultView.js ├── Search.js ├── SearchProvider.js ├── Settings.js ├── SideMenu.js ├── SubscriptionView.js └── Utils.js ├── configure.ac ├── data ├── Makefile.am ├── icons │ ├── Makefile.am │ ├── author.svg │ ├── back-symbolic.svg │ ├── bookmark.svg │ ├── bookmark_off.svg │ ├── edit-copy-symbolic.svg │ ├── folder-saved-search-symbolic.svg │ ├── headphones-symbolic.svg │ ├── hicolor │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ └── io.github.konkor.newstream.png │ │ └── scalable │ │ │ └── apps │ │ │ └── io.github.konkor.newstream.svg │ ├── history-symbolic.svg │ ├── io.github.konkor.newstream.svg │ ├── newstream.cover.svg │ ├── newstream.item.svg │ ├── open-menu-symbolic.svg │ ├── share.svg │ ├── social │ │ ├── fb.png │ │ ├── fb.svg │ │ ├── gplus.png │ │ ├── gplus.svg │ │ ├── id.png │ │ ├── id.svg │ │ ├── link.svg │ │ ├── mail.svg │ │ ├── red.png │ │ ├── red.svg │ │ ├── twit.png │ │ └── twit.svg │ └── window-close-symbolic.svg ├── io.github.konkor.newstream.desktop └── themes │ ├── Makefile.am │ ├── default │ └── gtk.css │ └── tube │ └── gtk.css ├── new-stream ├── packaging ├── debian │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── rules │ └── source │ │ └── format └── packaging.sh └── schemas ├── Makefile.am ├── gschemas.compiled └── io.github.konkor.newstream.gschema.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | *.zip 4 | *.tar.xz 5 | *.orig 6 | *.diff 7 | *.valid 8 | things 9 | packaging/debs 10 | packaging/VERSION 11 | clean.sh 12 | Makefile 13 | depcomp 14 | Makefile.in 15 | aclocal.m4 16 | zip-files/ 17 | .deps/ 18 | autom4te.cache/ 19 | compile 20 | config.guess 21 | config.h 22 | config.h.in 23 | config.log 24 | config.status 25 | config.sub 26 | configure 27 | install-sh 28 | libtool 29 | ltmain.sh 30 | missing 31 | stamp-h1 32 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | ## Process this file with automake to produce Makefile.in 2 | 3 | APP_ID = newstream 4 | 5 | DIST_SUBDIRS = data schemas common 6 | 7 | SUBDIRS = data schemas common 8 | 9 | jsdir = $(datadir)/$(APP_ID) 10 | js_DATA = \ 11 | README.md 12 | 13 | js_SCRIPTS = new-stream 14 | 15 | bin_SCRIPTS = new-stream 16 | 17 | dist_js_DATA = $(js_SCRIPTS) 18 | 19 | dist_doc_DATA = \ 20 | README.md 21 | 22 | uninstall-local: 23 | -rm -r $(jsdir) 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | GPLv3 License 3 | Stars
4 |

5 | 6 | ### [New Stream](https://github.com/konkor/newstream) Linux Youtube Video Player. 7 | ----- 8 | **Q**: Why, it's already available in a browser? 9 | 10 | **A**: I think native dedicated application is always better. It's why Android users have dedicating players and not using just a browser to it. 11 | 12 | `It's very first initial versions and all could be changed any time.` 13 | 14 | ![screencast](https://i.imgur.com/NZdkhYd.png) 15 | 16 |

17 | Latest deb package
18 | other releases 19 |

20 | 21 | ## Goals 22 | 23 | * Build a modern looking responsible application based on layout's views with a custom controls, header bar and UI like mobile applications. It should support history of layouts and navigation via keyboard so. 24 | * Local data organization via #hashtags to an example to assign custom category, for filtering, for playlists etc. 25 | * More personal #privacy and #security through the local store of links, lists, subscriptions and without authentication. Sure data collectors/service providers still will be see you external network IP address but for all your local network only not personalized by cookies and other browser info. So user could use proxy to hide IP addresses too. 26 | * Descent video player with all expected features. 27 | * Audio-mode for listening podcasts and music. 28 | * Fine audio processing including volume control, pre-amplifier, 10 bands cubic equalizer, pitch, re-sampling so. 29 | * Excellent linux system integration like glib notifications, menus, background player, appindicators and/or desktop extensions etc. 30 | * Nice to have offline-mode to able working without current network connection at all. 31 | 32 | ## Features 33 | 34 | * Searching (video items only) 35 | * Searching history 36 | * Basic video player (fullscreen on double-click, play/pause button and a seeking bar) 37 | 38 | See [releases page](https://github.com/konkor/newstream/releases/) and commits for current status of the development 39 | 40 | ## Planned Features 41 | * Modern styled GTK UI with user-friendly mobile-like behavior 42 | * Desktop integration 43 | * Advanced searching 44 | * Local subscriptions to channels 45 | * Local subscriptions to searching queries 46 | * Light Embedded Video Player based on gstreamer 47 | * more 48 | 49 | ## If you like the idea, please consider to become a baker and/or contributor 50 | I like completely open-source projects. It's why I picked GPLv3 license for my open projects. I think only such license could protect Desktop Users from Business Users. Maybe I'm a dreamer and want to believe in the pure projects but the reality is most projects and FOS organizations are sponsored by big business and founded by them. 51 | But real life is a hard thing and very complicated by many circumstances. I'm not young and all we have it's our life (time) and where we'll be tomorrow. Life is hard. I'd like it to work on my projects productively which want a lot of time and affords. Now I want get your support to have ability to support and develop projects. 52 | 53 | - [Become a backer or sponsor on Patreon](https://www.patreon.com/konkor). 54 | - One-time donation via [PayPal EURO](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WVAS5RXRMYVC4), [PayPal USD](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HGAFMMMQ9MQJ2) or Patron where you can choose a custom pledge. 55 | 56 | * Contact to the author [here](https://konkor.github.io/index.html#contact). 57 | 58 | _Behind the development for the Linux Desktop are ordinary people who spend a lot of time and their own resources to make the Linux Desktop better. Thank you for your contributions!_ 59 | 60 | ## Install 61 | 62 | _Debian/Ubuntu flavours_ 63 | Dowload [deb package](https://github.com/konkor/newstream/releases/) and install it. 64 | 65 | ```sh 66 | sudo dpkg -i newstream_VERSION.deb 67 | sudo apt-get -f install 68 | ``` 69 | 70 | ## Dependencies 71 | * gjs (core dependency) 72 | * GTK3 libraries: 73 | * gir1.2-gtk-3.0 74 | * gir1.2-gtkclutter-1.0, 75 | * gir1.2-clutter-gst, 76 | * gir1.2-gdkpixbuf-2.0, 77 | * gir1.2-soup-2.4 78 | * gir1.2-gstreamer-1.0 79 | * gir1.2-gstreamer-1.0 80 | * gstreamer1.0-libav 81 | * gstreamer1.0-plugins-bad 82 | * gstreamer1.0-plugins-ugly 83 | 84 | ## Testing 85 | 86 | ```sh 87 | git clone https://github.com/konkor/newstream 88 | cd newstream 89 | ./new-stream 90 | ``` 91 | 92 | ## License 93 | 94 | [GPLv3](https://www.gnu.org/licenses/gpl.html) 95 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | [See Projects Pages](https://github.com/konkor/newstream/projects) 2 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run this to generate all the initial makefiles, etc. 3 | 4 | srcdir=`dirname $0` 5 | test -z "$srcdir" && srcdir=. 6 | 7 | PKG_NAME="newstream" 8 | 9 | test -f $srcdir/configure.ac || { 10 | echo -n "**Error**: Directory "\`$srcdir\'" does not look like the" 11 | echo " top-level gnome-shell-extensions directory" 12 | exit 1 13 | } 14 | 15 | which gnome-autogen.sh || { 16 | echo "You need to install gnome-common from GNOME Git (or from" 17 | echo "your OS vendor's package manager)." 18 | exit 1 19 | } 20 | . gnome-autogen.sh 21 | 22 | -------------------------------------------------------------------------------- /common/Application.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gtk = imports.gi.Gtk; 12 | const GLib = imports.gi.GLib; 13 | const Gio = imports.gi.Gio; 14 | const Lang = imports.lang; 15 | 16 | const APPDIR = getCurrentFile ()[1]; 17 | imports.searchPath.unshift(APPDIR); 18 | 19 | const Logger = imports.common.Logger; 20 | const Window = imports.common.MainWindow; 21 | 22 | let inhibit_id = 0; 23 | 24 | var NewStreamApplication = new Lang.Class ({ 25 | Name: "NewStreamApplication", 26 | Extends: Gtk.Application, 27 | 28 | _init: function (params) { 29 | this.parent ({ 30 | application_id: "io.github.konkor.newstream", 31 | flags: Gio.ApplicationFlags.HANDLES_OPEN 32 | }); 33 | GLib.set_prgname ("New Stream"); 34 | GLib.set_application_name ("New Stream"); 35 | 36 | this.player_enabled = false; 37 | 38 | this.add_main_option ( 39 | 'debug', 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 40 | "Enable debugging messages", null 41 | ); 42 | this.add_main_option ( 43 | 'verbose', 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 44 | "Enable verbose output", null 45 | ); 46 | this.connect ('handle-local-options', this.on_local_options.bind (this)); 47 | }, 48 | 49 | on_local_options: function (app, options) { 50 | try { 51 | this.register (null); 52 | } catch (e) { 53 | Logger.error ("Failed to register: %s".format (e.message)); 54 | return 1; 55 | } 56 | 57 | if (options.contains ("verbose")) { 58 | Logger.init (1); 59 | } 60 | if (options.contains ("debug")) { 61 | Logger.init (2); 62 | } 63 | 64 | return -1; 65 | }, 66 | 67 | vfunc_startup: function () { 68 | this.parent (); 69 | 70 | let action_entries = [ 71 | { name: "bookmarks", 72 | activate: () => { 73 | this.unfullscreen (); 74 | this.window.on_stack_update (this, "bookmarks"); 75 | }, 76 | accels: ["b", "b"] 77 | }, 78 | { name: "subscriptions", 79 | activate: () => { 80 | this.unfullscreen (); 81 | this.window.on_stack_update (this, "subscriptions"); 82 | }, 83 | accels: ["s", "s"] 84 | }, 85 | { name: "history", 86 | activate: () => { 87 | this.unfullscreen (); 88 | this.window.on_stack_update (this, "history"); 89 | }, 90 | accels: ["h", "h"] 91 | }, 92 | { name: "preferences", 93 | activate: () => { 94 | this.unfullscreen (); 95 | this.window.on_stack_update (this, "preferences"); 96 | }, 97 | accels: ["p"] 98 | }, 99 | { name: "search", 100 | activate: () => { 101 | this.unfullscreen (); 102 | this.window.on_stack_update (this, "search"); 103 | }, 104 | accels: ["s"], 105 | enabled: false 106 | }, 107 | { name: "search-enabled", 108 | activate: () => { 109 | this.search_enabled = true; 110 | this.lookup_action ("search").set_enabled (true); 111 | } 112 | }, 113 | { name: "channel", 114 | activate: () => { 115 | this.unfullscreen (); 116 | this.window.on_stack_update (this, "channel"); 117 | }, 118 | accels: ["c", "c"], 119 | enabled: false 120 | }, 121 | { name: "channel-enabled", 122 | activate: () => { 123 | this.channel_enabled = true; 124 | this.lookup_action ("channel").set_enabled (true); 125 | } 126 | }, 127 | { name: "player", 128 | activate: () => { 129 | this.unfullscreen (); 130 | if (this.window.itemview.player.item) 131 | this.window.on_stack_update (this, "item"); 132 | }, 133 | accels: ["p", "p"], 134 | enabled: false 135 | }, 136 | { name: "player-enabled", 137 | activate: () => { 138 | this.player_enabled = true; 139 | this.lookup_action ("player").set_enabled (true); 140 | } 141 | }, 142 | { name: "back-layout", 143 | activate: () => {this.on_back_layout ()}, 144 | accels: ["Escape"] 145 | }, 146 | { name: "inhibit", 147 | activate: () => { 148 | this.uninhibit_cb (); 149 | inhibit_id = this.inhibit (this.window, Gtk.ApplicationInhibitFlags.IDLE, "Media playing"); 150 | } 151 | }, 152 | { name: "uninhibit", 153 | activate: () => { 154 | this.uninhibit_cb (); 155 | } 156 | }, 157 | { name: "toggle-fullscreen", 158 | activate: () => {this.on_toggle_fullscreen ()}, 159 | accels: ["f", "Return"] 160 | }, 161 | { name: "toggle-play", 162 | activate: () => {this.on_toggle_play ()}, 163 | accels: ["space"] 164 | }, 165 | { name: "seek-forward", 166 | activate: () => { 167 | var player = this.window.itemview.player; 168 | if (player.engine.state == 4) player.seek_delta (10); 169 | else player.seek_frame (1); 170 | }, 171 | accels: ["Right"] 172 | }, 173 | { name: "seek-backward", 174 | activate: () => { 175 | var player = this.window.itemview.player; 176 | if (player.engine.state == 4) player.seek_delta (-10); 177 | else player.seek_frame (-1); 178 | }, 179 | accels: ["Left"] 180 | }, 181 | { name: "volume-up", 182 | activate: () => { 183 | var player = this.window.itemview.player; 184 | if (player) player.set_volume_delta (0.05); 185 | }, 186 | accels: ["Up"] 187 | }, 188 | { name: "volume-down", 189 | activate: () => { 190 | var player = this.window.itemview.player; 191 | if (player) player.set_volume_delta (-0.05); 192 | }, 193 | accels: ["Down"] 194 | }, 195 | { name: "quality-preset", 196 | activate: (action, parameter) => { 197 | action.change_state (parameter); 198 | }, 199 | change_state: (o, preset) => { 200 | var player = this.window.itemview.player; 201 | if (player) player.set_preset (preset.deep_unpack ()); 202 | o.set_state (preset); 203 | }, 204 | state: new GLib.Variant ("i", -1), 205 | parameter_type: new GLib.VariantType ("i") 206 | } 207 | ]; 208 | action_entries.forEach (entry => { 209 | let props = {}; 210 | ['name', 'state', 'parameter_type'].forEach ((prop) => { 211 | if (entry[prop]) props[prop] = entry[prop]; 212 | }); 213 | let action = new Gio.SimpleAction (props); 214 | if (entry.create_hook) entry.create_hook (action); 215 | if (entry.activate) action.connect ('activate', entry.activate); 216 | if (entry.change_state) action.connect ('change-state', entry.change_state); 217 | if (entry.accels) this.set_accels_for_action ('app.' + entry.name, entry.accels); 218 | if (typeof entry.enabled !== 'undefined' ) action.set_enabled (entry.enabled); 219 | this.add_action (action); 220 | }); 221 | this.search_enabled = false; 222 | }, 223 | 224 | vfunc_activate: function () { 225 | if (!this.active_window) { 226 | this.window = new Window.MainWindow ({ application:this }); 227 | this.window.show_all (); 228 | } else { 229 | this.window.present (); 230 | } 231 | }, 232 | 233 | uninhibit_cb: function () { 234 | if (inhibit_id) this.uninhibit (inhibit_id); 235 | inhibit_id = 0; 236 | }, 237 | 238 | on_toggle_fullscreen: function () { 239 | if (this.window.stack.visible_child_name == "item") 240 | this.window.itemview.player.video.toggle_fullscreen (); 241 | }, 242 | 243 | unfullscreen: function () { 244 | if (this.window.itemview.player.video.fullscreen) 245 | this.window.itemview.player.video.toggle_fullscreen (); 246 | }, 247 | 248 | on_back_layout: function () { 249 | if (this.window.itemview.player.video.fullscreen) 250 | this.window.itemview.player.video.toggle_fullscreen (); 251 | else if (this.window.back.visible) this.window.on_back (); 252 | }, 253 | 254 | on_toggle_play: function () { 255 | if (this.window.itemview.player.item) 256 | this.window.itemview.player.toggle_play (); 257 | }, 258 | 259 | enable_global_actions: function () { 260 | this.lookup_action ("back-layout").set_enabled (true); 261 | this.lookup_action ("toggle-fullscreen").set_enabled (true); 262 | this.lookup_action ("toggle-play").set_enabled (true); 263 | this.lookup_action ("bookmarks").set_enabled (true); 264 | this.lookup_action ("subscriptions").set_enabled (true); 265 | this.lookup_action ("history").set_enabled (true); 266 | this.lookup_action ("player").set_enabled (this.player_enabled); 267 | this.lookup_action ("search").set_enabled (this.search_enabled); 268 | this.lookup_action ("channel").set_enabled (this.channel_enabled); 269 | this.lookup_action ("seek-forward").set_enabled (true); 270 | this.lookup_action ("seek-backward").set_enabled (true); 271 | this.lookup_action ("volume-up").set_enabled (true); 272 | this.lookup_action ("volume-down").set_enabled (true); 273 | }, 274 | 275 | disable_global_actions: function () { 276 | this.lookup_action ("back-layout").set_enabled (false); 277 | this.lookup_action ("toggle-fullscreen").set_enabled (false); 278 | this.lookup_action ("toggle-play").set_enabled (false); 279 | this.lookup_action ("bookmarks").set_enabled (false); 280 | this.lookup_action ("subscriptions").set_enabled (false); 281 | this.lookup_action ("history").set_enabled (false); 282 | this.lookup_action ("player").set_enabled (false); 283 | this.lookup_action ("channel").set_enabled (false); 284 | this.lookup_action ("search").set_enabled (false); 285 | this.lookup_action ("seek-forward").set_enabled (false); 286 | this.lookup_action ("seek-backward").set_enabled (false); 287 | this.lookup_action ("volume-up").set_enabled (false); 288 | this.lookup_action ("volume-down").set_enabled (false); 289 | }, 290 | 291 | get current_dir () { 292 | return APPDIR; 293 | } 294 | }); 295 | 296 | function getCurrentFile () { 297 | let stack = (new Error()).stack; 298 | let stackLine = stack.split("\n")[1]; 299 | if (!stackLine) 300 | throw new Error ("Could not find current file"); 301 | let match = new RegExp ("@(.+):\\d+").exec(stackLine); 302 | if (!match) 303 | throw new Error ("Could not find current file"); 304 | let path = match[1]; 305 | let file = Gio.File.new_for_path (path).get_parent(); 306 | return [file.get_path(), file.get_parent().get_path(), file.get_basename()]; 307 | } 308 | -------------------------------------------------------------------------------- /common/BookmarkView.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Lang = imports.lang; 12 | const Gio = imports.gi.Gio; 13 | const Gtk = imports.gi.Gtk; 14 | const GdkPixbuf = imports.gi.GdkPixbuf; 15 | 16 | const Logger = imports.common.Logger; 17 | const Utils = imports.common.Utils; 18 | const ResultView = imports.common.ResultView; 19 | 20 | const IPP = 20; //items per page 21 | 22 | var BookmarkView = new Lang.Class({ 23 | Name: "BookmarkView", 24 | Extends: ResultView.ResultView, 25 | 26 | _init: function (parent) { 27 | this.parent (parent); 28 | this.settings = parent.settings; 29 | this.results.max_children_per_line = 1; 30 | this.results.homogeneous = false; 31 | }, 32 | 33 | query: function (page) { 34 | page = page || 0; 35 | //this.url = "view_history"; 36 | if (page*IPP >= this.settings.bookmarks.length) return; 37 | let marks = this.settings.bookmarks.slice (page*IPP, (page+1)*IPP); 38 | //print (page*IPP, (page+1)*IPP,history.length,this.settings.view_history); 39 | if (!marks.length) return; 40 | if (page) this.pager.prev.token = (page - 1).toString (); 41 | else this.pager.prev.token = ""; 42 | if ((this.settings.bookmarks.length - (page+1)*IPP) > 0) this.pager.next.token = (page + 1).toString (); 43 | else this.pager.next.token = ""; 44 | this.pager.toggle (); 45 | this.clear_all (); 46 | marks.forEach (p => { 47 | let data = this.settings.get_view_history_item (p); 48 | if (!data) { 49 | this.settings.toggle_bookmark (p, false); 50 | return; 51 | } 52 | let item = new BookmarkViewItem (data); 53 | item.show_details (); 54 | this.results.add (item); 55 | }); 56 | if (this.scroll) this.scroll.vadjustment.value = 0; 57 | }, 58 | 59 | on_page_selected: function (o, token) { 60 | this.query (parseInt (token)); 61 | } 62 | }); 63 | 64 | var BookmarkViewItem = new Lang.Class({ 65 | Name: "BookmarkViewItem", 66 | Extends: ResultView.ResultViewItem, 67 | 68 | _init: function (data) { 69 | this.parent (data); 70 | this.margin = 2; 71 | this.title.max_width_chars = 64; 72 | this.title.lines = 1; 73 | this.dbox.no_show_all = true; 74 | this.dbox.visible = false; 75 | this.image.pixbuf = this.image.pixbuf.scale_simple (48, 30, 2); 76 | 77 | this.local = new Gtk.Label ({label:this.details.data.local.views + " views", xalign:1, opacity: 0.7}); 78 | this.local.get_style_context ().add_class ("small"); 79 | this.cbox.pack_end (this.local, false, false, 0); 80 | this.local.show (); 81 | }, 82 | 83 | get_thumb: function () { 84 | let url = this.details.get_thumbnail_url ("default"); 85 | if (url) Utils.fetch (url, null, null, (d, r) => { 86 | if (r != 200) return; 87 | try { 88 | this.image.pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale (Gio.MemoryInputStream.new_from_bytes (d), 48, 48, true, null); 89 | } catch (e) {debug (e.message);} 90 | }); 91 | } 92 | }); 93 | 94 | const DOMAIN = "BookmarkView"; 95 | function error (msg) {Logger.error (DOMAIN, msg)} 96 | function debug (msg) {Logger.debug (DOMAIN, msg)} 97 | function info (msg) {Logger.info (DOMAIN, msg)} 98 | -------------------------------------------------------------------------------- /common/ChannelView.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gio = imports.gi.Gio; 12 | const Gtk = imports.gi.Gtk; 13 | const GdkPixbuf = imports.gi.GdkPixbuf; 14 | const Lang = imports.lang; 15 | 16 | const Utils = imports.common.Utils; 17 | const ItemView = imports.common.ItemView; 18 | 19 | var ChannelDetails = new Lang.Class({ 20 | Name: "ChannelDetails", 21 | Extends: Gtk.Box, 22 | 23 | _init: function (view) { 24 | this.parent ({orientation:Gtk.Orientation.HORIZONTAL}); 25 | this.get_style_context ().add_class ("search-bar"); 26 | this.view = view; 27 | 28 | //let space = new Gtk.Box (); 29 | //this.pack_start (space, true, true, 0); 30 | 31 | //this.frame = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL, margin:8}); 32 | //this.frame.margin_left = 48; 33 | this.logo = new Gtk.Button ({always_show_image: true,yalign: 0.0}); 34 | this.logo.image = new Gtk.Image (); 35 | this.logo.get_style_context ().add_class ("channel-button"); 36 | this.logo.set_relief (Gtk.ReliefStyle.NONE); 37 | this.add (this.logo); 38 | 39 | let contents = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL}); 40 | this.pack_start (contents, true, true, 0); 41 | 42 | //let space = new Gtk.Box (); 43 | //this.pack_start (space, true, true, 0); 44 | 45 | this.itembar = new ItemView.Itembar (); 46 | contents.pack_start (this.itembar, true, true, 0); 47 | 48 | let box = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL}); 49 | contents.pack_start (box, true, true, 0); 50 | 51 | this.channel = new ItemView.Channel (); 52 | box.pack_start (this.channel, true, true, 0); 53 | //this.channel.logo.reparent (this.frame); 54 | //this.channel.logo.yalign = 0.0; 55 | this.channel.logo.pixbuf = null; 56 | 57 | this.statistics = new ItemView.Statistics (); 58 | box.pack_end (this.statistics, false, false, 0); 59 | 60 | this.description = new ItemView.Description (); 61 | contents.add (this.description); 62 | }, 63 | 64 | load: function (channel, pixbuf) { 65 | if (!channel || !channel.id) return; 66 | //this.channel.load (item); 67 | this.channel.channel = channel; 68 | channel.videos = channel.videos || "0"; 69 | this.channel.author.set_text (Utils.format_size_long (channel.videos) + " videos created"); 70 | if (channel.published) { 71 | this.channel.published.set_text ("since " + (new Date (channel.published)).toLocaleDateString()); 72 | } else this.channel.published.set_text (""); 73 | if (pixbuf) this.logo.image.pixbuf = pixbuf.scale_simple (128,128,3); 74 | 75 | //this.statistics.load (item); 76 | channel.views = channel.views || "0"; 77 | this.statistics.views.set_text (Utils.format_size_long (channel.views) + " views"); 78 | channel.subscribers = channel.subscribers || "0"; 79 | this.statistics.likes.set_text (Utils.format_size_long (channel.subscribers) + " subscribers"); 80 | this.statistics.url = "https://youtube.com/channel" + channel.id; 81 | 82 | //this.description.load (item); 83 | channel.description = channel.description || ""; 84 | this.description.load (channel); 85 | 86 | this.itembar.base_url = "https://youtube.com/channel/"; 87 | this.itembar.set_link (channel.id, channel, this.view.w.settings.subscribed (channel.id), 1); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /common/HistoryView.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gio = imports.gi.Gio; 12 | const Gtk = imports.gi.Gtk; 13 | const GdkPixbuf = imports.gi.GdkPixbuf; 14 | const Lang = imports.lang; 15 | 16 | const Utils = imports.common.Utils; 17 | const BookmarkView = imports.common.BookmarkView; 18 | 19 | const IPP = 20; //items per page 20 | 21 | var HistoryView = new Lang.Class({ 22 | Name: "HistoryView", 23 | Extends: BookmarkView.BookmarkView, 24 | 25 | _init: function (parent) { 26 | this.parent (parent); 27 | 28 | this.date = ""; 29 | this.date_options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; 30 | }, 31 | 32 | query: function (page) { 33 | page = page || 0; 34 | //this.url = "view_history"; 35 | if (page*IPP >= this.settings.view_history.length) return; 36 | let history = this.settings.view_history.slice (page*IPP, (page+1)*IPP); 37 | //print (page*IPP, (page+1)*IPP,history.length,this.settings.view_history); 38 | if (!history.length) return; 39 | if (page) this.pager.prev.token = (page - 1).toString (); 40 | else this.pager.prev.token = ""; 41 | if ((this.settings.view_history.length - (page+1)*IPP) > 0) this.pager.next.token = (page + 1).toString (); 42 | else this.pager.next.token = ""; 43 | this.pager.toggle (); 44 | this.clear_all (); 45 | history.forEach (p => { 46 | let item = new BookmarkView.BookmarkViewItem (this.settings.get_view_history_item (p)); 47 | item.show_details (); 48 | var d = new Date (item.details.data.local.last).toLocaleDateString ("lookup", this.date_options); 49 | if (this.date != d) { 50 | this.add_date (d); 51 | } 52 | this.results.add (item); 53 | }); 54 | if (this.scroll) this.scroll.vadjustment.value = 0; 55 | }, 56 | 57 | add_date: function (date) { 58 | this.date = date; 59 | let label = new Gtk.Label({label:this.date, xalign:0.0, margin:6, sensitive:true}); 60 | label.show_all (); 61 | this.results.add (label); 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /common/Layouts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gio = imports.gi.Gio; 12 | const Gtk = imports.gi.Gtk; 13 | const Lang = imports.lang; 14 | 15 | const Preferences = imports.common.Preferences; 16 | const ResultView = imports.common.ResultView; 17 | const HistoryView = imports.common.HistoryView; 18 | const BookmarkView = imports.common.BookmarkView; 19 | const SubscriptionView = imports.common.SubscriptionView; 20 | const ChannelView = imports.common.ChannelView; 21 | const Item = imports.common.ItemView; 22 | 23 | var HistoryLayout = new Lang.Class({ 24 | Name: "HistoryLayout", 25 | Extends: HistoryView.HistoryView, 26 | 27 | _init: function (parent) { 28 | this.parent (parent); 29 | 30 | this.connect ("map", this.setup.bind (this)); 31 | }, 32 | 33 | setup: function (o, e) { 34 | if (this.w.settings.view_history_modified) this.query (); 35 | this.w.settings.view_history_modified = false; 36 | this.w.section.label = "History"; 37 | this.w.home.visible = false; 38 | this.w.back.visible = true; 39 | this.w.searchbar.visible = false; 40 | this.w.topbar.visible = false; 41 | this.w.menu_button.visible = true; 42 | this.w.fullscreen_button.visible = false; 43 | this.w.player_menu.visible = false; 44 | this.w.spinner.visible = true; 45 | } 46 | }); 47 | 48 | var BookmarkLayout = new Lang.Class({ 49 | Name: "BookmarkLayout", 50 | Extends: BookmarkView.BookmarkView, 51 | 52 | _init: function (parent) { 53 | this.parent (parent); 54 | 55 | this.connect ("map", this.setup.bind (this)); 56 | }, 57 | 58 | setup: function (o, e) { 59 | if (this.w.settings.bookmarks_modified) this.query (); 60 | this.w.settings.bookmarks_modified = false; 61 | this.w.section.label = "Bookmarks"; 62 | this.w.home.visible = false; 63 | this.w.back.visible = true; 64 | this.w.searchbar.visible = false; 65 | this.w.topbar.visible = false; 66 | this.w.menu_button.visible = true; 67 | this.w.fullscreen_button.visible = false; 68 | this.w.player_menu.visible = false; 69 | } 70 | }); 71 | 72 | var SubscriptionLayout = new Lang.Class({ 73 | Name: "SubscriptionLayout", 74 | Extends: SubscriptionView.SubscriptionView, 75 | 76 | _init: function (parent) { 77 | this.parent (parent); 78 | this.connect ("map", this.setup.bind (this)); 79 | }, 80 | 81 | setup: function (o, e) { 82 | if (this.w.settings.channels_modified) this.query (); 83 | this.w.settings.channels_modified = false; 84 | this.w.section.label = "Subscriptions"; 85 | this.w.home.visible = false; 86 | this.w.back.visible = true; 87 | this.w.searchbar.visible = false; 88 | this.w.topbar.visible = false; 89 | this.w.menu_button.visible = true; 90 | this.w.fullscreen_button.visible = false; 91 | this.w.player_menu.visible = false; 92 | } 93 | }); 94 | 95 | var PreferencesLayout = new Lang.Class({ 96 | Name: "PreferencesLayout", 97 | Extends: Preferences.Preferences, 98 | 99 | _init: function (parent) { 100 | this.parent (parent.settings); 101 | this.w = parent; 102 | this.connect ("map", this.setup.bind (this)); 103 | }, 104 | 105 | setup: function (o, e) { 106 | this.update (); 107 | this.w.section.label = "Preferences"; 108 | this.w.home.visible = false; 109 | this.w.back.visible = true; 110 | this.w.searchbar.visible = false; 111 | this.w.topbar.visible = false; 112 | this.w.menu_button.visible = true; 113 | this.w.fullscreen_button.visible = false; 114 | this.w.player_menu.visible = false; 115 | } 116 | }); 117 | 118 | var ItemLayout = new Lang.Class({ 119 | Name: "ItemLayout", 120 | Extends: Item.ItemView, 121 | 122 | _init: function (parent) { 123 | this.parent (parent); 124 | this.w = parent; 125 | 126 | this.connect ("map", this.setup.bind (this)); 127 | }, 128 | 129 | query: function (item) { 130 | this.load (item); 131 | }, 132 | 133 | setup: function (o, e) { 134 | if (this.player.item) this.w.section.label = this.player.item.title; 135 | this.w.home.visible = false; 136 | this.w.back.visible = true; 137 | this.w.searchbar.visible = false; 138 | this.w.topbar.visible = false; 139 | this.w.menu_button.visible = false; 140 | this.w.fullscreen_button.visible = true; 141 | this.w.player_menu.visible = true; 142 | } 143 | }); 144 | 145 | var HotView = new Lang.Class({ 146 | Name: "HotView", 147 | Extends: ResultView.ResultView, 148 | 149 | _init: function (parent) { 150 | this.parent (parent); 151 | this.w = parent; 152 | 153 | this.connect ("map", this.setup.bind (this)); 154 | }, 155 | 156 | query: function (words) { 157 | this.url = this.provider.get_hot (this.on_results.bind (this)); 158 | }, 159 | 160 | setup: function (o, e) { 161 | this.w.section.label = "New Stream"; 162 | this.w.home.visible = true; 163 | this.w.back.visible = false; 164 | this.w.searchbar.visible = true; 165 | this.w.topbar.visible = true; 166 | this.w.menu_button.visible = true; 167 | this.w.fullscreen_button.visible = false; 168 | this.w.player_menu.visible = false; 169 | } 170 | }); 171 | 172 | var NewView = new Lang.Class({ 173 | Name: "NewView", 174 | Extends: HotView, 175 | 176 | _init: function (parent) { 177 | this.parent (parent); 178 | }, 179 | 180 | query: function (words) { 181 | this.url = this.provider.get_day (this.on_results.bind (this)); 182 | } 183 | }); 184 | 185 | var HitView = new Lang.Class({ 186 | Name: "HitView", 187 | Extends: HotView, 188 | 189 | _init: function (parent) { 190 | this.parent (parent); 191 | }, 192 | 193 | query: function (words) { 194 | this.url = this.provider.get_hit (this.on_results.bind (this)); 195 | } 196 | }); 197 | 198 | var SearchView = new Lang.Class({ 199 | Name: "SearchView", 200 | Extends: ResultView.ResultView, 201 | 202 | _init: function (parent) { 203 | this.parent (parent); 204 | this.w = parent; 205 | this.words = "New Stream"; 206 | 207 | this.connect ("map", this.setup.bind (this)); 208 | }, 209 | 210 | query: function (words) { 211 | if (!words) return; 212 | this.words = words; 213 | this.url = this.provider.get (words, this.on_results.bind (this)); 214 | }, 215 | 216 | setup: function (o, e) { 217 | this.w.section.label = this.words; 218 | this.w.home.visible = false; 219 | this.w.back.visible = true; 220 | this.w.searchbar.visible = false; 221 | this.w.topbar.visible = false; 222 | this.w.menu_button.visible = true; 223 | this.w.fullscreen_button.visible = false; 224 | this.w.player_menu.visible = false; 225 | } 226 | }); 227 | 228 | var ChannelLayout = new Lang.Class({ 229 | Name: "ChannelLayout", 230 | Extends: SearchView, 231 | 232 | _init: function (parent) { 233 | this.parent (parent); 234 | this.channel = null; 235 | 236 | this.details = new ChannelView.ChannelDetails (this); 237 | this.header.add (this.details); 238 | }, 239 | 240 | query: function (words) { 241 | if (this.channel) 242 | this.url = this.provider.get_channel (this.channel.id, this.on_results.bind (this)); 243 | }, 244 | 245 | load: function (channel, pixbuf) { 246 | if (!channel) return; 247 | if (!this.channel || (this.channel.id != channel.id)) 248 | this.details.load (channel, pixbuf); 249 | this.channel = channel; 250 | //print (JSON.stringify (channel)); 251 | this.query (); 252 | if (this.channel.title) this.words = this.channel.title; 253 | }, 254 | 255 | get_channel_data: function () { 256 | if (!this.channel) return null; 257 | if (!this.channel.local) this.channel.local = {}; 258 | this.channel.local.last = this.first_item_data.id; 259 | return this.channel; 260 | } 261 | }); 262 | -------------------------------------------------------------------------------- /common/Logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | var Format = imports.format; 12 | String.prototype.format = Format.format; 13 | 14 | //DOMAIN ERROR:0:RED, INFO:1:BLUE, DEBUG:2:GREEN 15 | const domain_color = ["00;31","00;34","00;32"]; 16 | const domain_name = ["EE","II","DD"]; 17 | const domain_source = "application"; 18 | 19 | var LEVEL = { ERROR: 0, INFO: 1, DEBUG: 2 }; 20 | var debug_lvl = LEVEL.ERROR; 21 | let mono = false; 22 | 23 | function init (level, nocolor) { 24 | level = level || 0; 25 | debug_lvl = level; 26 | if (nocolor) mono = true; 27 | } 28 | 29 | function info (source, msg) { 30 | if (!msg) { 31 | msg = source; 32 | source = domain_source; 33 | } 34 | if (debug_lvl > 0) print_msg (1, source, msg); 35 | } 36 | 37 | function debug (source, msg) { 38 | if (!msg) { 39 | msg = source; 40 | source = domain_source; 41 | } 42 | if (debug_lvl > 1) print_msg (2, source, msg); 43 | } 44 | 45 | function error (source, msg) { 46 | if (!msg) { 47 | msg = source; 48 | source = domain_source; 49 | } 50 | print_msg (0, source, msg); 51 | } 52 | 53 | function print_msg (domain, source, output) { 54 | let d = new Date(); 55 | let ds = d.toString (); 56 | let i = ds.indexOf (" GMT"); 57 | if (i > 0) ds = ds.substring (0, i); 58 | 59 | if (mono) print ("[%s.%s](%s) [xstream][%s] %s".format ( 60 | ds,(d.getMilliseconds() / 1000).toFixed(3).slice(2, 5),domain_name[domain],source,output)); 61 | else print ("\x1b[%sm[%s.%s](%s) [xstream][%s]\x1b[0m %s".format ( 62 | domain_color[domain],ds,(d.getMilliseconds() / 1000).toFixed(3).slice(2, 5),domain_name[domain],source,output)); 63 | //log ("(%s) [cpufreq][%s] %s".format (domain_name[domain], source, output)); 64 | } 65 | -------------------------------------------------------------------------------- /common/Makefile.am: -------------------------------------------------------------------------------- 1 | #common classes 2 | commondir = $(datadir)/newstream/common 3 | common_DATA = \ 4 | Application.js \ 5 | BookmarkView.js \ 6 | ChannelView.js \ 7 | HistoryView.js \ 8 | ItemView.js \ 9 | Layouts.js \ 10 | Logger.js \ 11 | MainWindow.js \ 12 | PlayerEngine.js \ 13 | Player.js \ 14 | ResultView.js \ 15 | Search.js \ 16 | SearchProvider.js \ 17 | Settings.js \ 18 | SideMenu.js \ 19 | SubscriptionView.js \ 20 | Utils.js \ 21 | $(NULL) 22 | 23 | 24 | EXTRA_DIST = \ 25 | $(common_DATA) 26 | -------------------------------------------------------------------------------- /common/PlayerEngine.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Lang = imports.lang; 12 | const Signals = imports.signals; 13 | 14 | const GObject = imports.gi.GObject; 15 | const GLib = imports.gi.GLib; 16 | //const Gio = imports.gi.Gio; 17 | const Gst = imports.gi.Gst; 18 | const GstVideo = imports.gi.GstVideo; 19 | 20 | const Logger = imports.common.Logger; 21 | 22 | let timer = 0; 23 | 24 | var PlayerEngine = new Lang.Class({ 25 | Name: "PlayerEngine", 26 | Extends: GObject.GObject, 27 | Signals: { 28 | 'state-changed': { 29 | flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED, 30 | param_types: [GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_INT]}, 31 | 'progress': { 32 | flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED, 33 | param_types: [GObject.TYPE_INT, GObject.TYPE_INT]}, 34 | 'buffering': { 35 | flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED, 36 | param_types: [GObject.TYPE_INT]}, 37 | }, 38 | 39 | _init: function () { 40 | Gst.init(null); //["GST_GL_PLATFORM=\"egl\""] 41 | this.audio_state = 0; 42 | this.video_state = 0; 43 | this.audio_buffer = 0; 44 | this.video_buffer = 0; 45 | this.video_stream = false; 46 | this.audio_stream = false; 47 | this.play_on_ready = false; 48 | 49 | //Fake outputs 50 | this.fakeaudio = Gst.ElementFactory.make ("fakesink", "fakeaudio"); 51 | this.fakevideo = Gst.ElementFactory.make ("fakesink", "fakevideo"); 52 | 53 | this.videobin = new Gst.Pipeline ({name:"xstream_video"}); 54 | this.audiobin = new Gst.Pipeline ({name:"xstream_audio"}); 55 | 56 | //Main Video(audio, subtitles) stream 57 | //this.playbin = Gst.ElementFactory.make("playbin", "mainbin"); 58 | this.audiosink = Gst.ElementFactory.make ("pulsesink", "audiosink"); 59 | if (!this.audiosink) 60 | this.audiosink = Gst.ElementFactory.make ("alsasink", "audiosink"); 61 | 62 | //Audio stream 63 | //this.audiobin = Gst.ElementFactory.make ("playbin", "audiobin"); 64 | //this.audiosink2 = Gst.ElementFactory.make ("pulsesink", "audiosink2"); 65 | //this.audiobin = Gst.parse_launch ("uridecodebin name=\"audiouri\" ! audioconvert ! audio/x-raw ! pulsesink"); 66 | this.audiodec = Gst.ElementFactory.make ("playbin", "audiobin"); 67 | this.audiodec.set_property ("flags", 2); 68 | this.audiodec.set_property ("video-sink", this.fakevideo); 69 | this.audiodec.set_property ("audio-sink", this.audiosink); 70 | this.audiobin.add (this.audiodec); 71 | 72 | this.videosink = Gst.ElementFactory.make ("cluttersink", "videosink"); 73 | this.videodec = Gst.ElementFactory.make ("playbin", "videobin"); 74 | this.videodec.set_property ("video-sink", this.videosink); 75 | this.videodec.set_property ("audio-sink", this.fakeaudio); 76 | this.videodec.set_property ("flags", 1); 77 | this.videobin.add (this.videodec); 78 | 79 | 80 | ////this.pipeline.add (this.playbin); 81 | ////this.audiobin.set_property ("video-sink", this.fakevideo); 82 | //this.audiobin.set_property ("flags", 2); 83 | //this.audiobin.set_property ("audio-sink", this.audiosink2); 84 | 85 | this.current_volume = 0; 86 | this.repeat = false; 87 | 88 | this.bus = this.audiobin.get_bus (); 89 | this.bus.add_signal_watch (); 90 | this.bus.connect ("message", this.on_bus_message_audio.bind (this)); 91 | 92 | this.bus1 = this.videobin.get_bus (); 93 | this.bus1.add_signal_watch (); 94 | this.bus1.connect ("message", this.on_bus_message_video.bind (this)); 95 | 96 | timer = GLib.timeout_add (0, 1000, this.on_timer.bind (this)); 97 | }, 98 | 99 | get state () { 100 | if (!this.audio_stream) return this.video_state; 101 | if (!this.video_stream) return this.audio_state; 102 | if (this.audio_state <= this.video_state) return this.audio_state; 103 | return this.video_state; 104 | }, 105 | 106 | get position () { 107 | let pos, pos2, res, pipe = this.video_stream ? this.videobin : this.audiobin; 108 | [res, pos] = pipe.query_position (Gst.Format.TIME); 109 | if (!res) pos = -1; 110 | else { 111 | if (this.video_stream && this.audio_stream) { 112 | [res, pos2] = this.audiobin.query_position (Gst.Format.TIME); 113 | //this.seek (pos, true); 114 | //print ("audio:", pos2, "video:", pos, "delta:", pos2 - pos); 115 | } 116 | pos /= Gst.MSECOND; 117 | } 118 | 119 | return pos; 120 | }, 121 | 122 | get duration () { 123 | let dur, res, pipe = this.video_stream ? this.videobin : this.audiobin; 124 | [res, dur] = pipe.query_duration (Gst.Format.TIME); 125 | if (!res) dur = -1; 126 | else dur /= Gst.MSECOND; 127 | return dur; 128 | }, 129 | 130 | get volume () { 131 | if (!this.current_volume && this.audiodec) this.current_volume = this.audiodec.get_volume (1); 132 | return this.current_volume; 133 | }, 134 | 135 | set volume (val) { 136 | if (!this.audiobin) return; 137 | val = val || 0; 138 | if (val > 1) val = 1.0; 139 | this.audiodec.set_volume (1, val); 140 | //this.playbin.set_volume (1, val); 141 | this.current_volume = val; 142 | }, 143 | 144 | open: function (video_format, audio_format) { 145 | //video_url = "http://192.168.1.2:8088/0/test.mp4";audio_url = "http://192.168.1.2:8088/0/test.mp4"; 146 | this.stop (); 147 | info ("open video/audio streams:\n" + JSON.stringify(video_format) + "\n\n" + JSON.stringify(audio_format)); 148 | if (!video_format && !audio_format) return; 149 | if (video_format) this.set_video (video_format.fragment_base_url || video_format.url, true); 150 | else this.set_video (null, true); 151 | if (audio_format) this.set_audio (audio_format.fragment_base_url || audio_format.url, true); 152 | else this.set_audio (null, true); 153 | //this.play (); 154 | this.seek (0, false); 155 | }, 156 | 157 | set_audio: function (url, noplay) { 158 | debug ("set audio streams:\n" + url); 159 | if (url == this.audio_url) return; 160 | this.stop (); 161 | this.audio_url = url || null; 162 | if (url) { 163 | this.audiodec.set_property ("uri", url); 164 | //if (!this.audio_stream) this.pipeline.add (this.audiobin); 165 | //this.playbin.set_property ("audio-sink", this.fakeaudio); 166 | this.audio_stream = true; 167 | } else { 168 | //if (this.audio_stream) this.pipeline.remove (this.audiobin); 169 | //this.playbin.set_property ("audio-sink", this.audiosink); 170 | this.audio_stream = false; 171 | } 172 | //this.audiobin.sync_state_with_parent (); 173 | //this.play (true); 174 | this.preload (); 175 | if (!noplay) this.seek (0, false); 176 | }, 177 | 178 | set_video: function (url, noplay) { 179 | debug ("set video streams:\n" + url); 180 | this.stop (); 181 | if (url) { 182 | //if (!this.video_stream) this.pipeline.add (this.videobin); 183 | this.videodec.set_property ("uri", url); 184 | this.video_stream = true; 185 | } else { 186 | //if (this.video_stream) this.pipeline.remove (this.videobin); 187 | this.video_stream = false; 188 | } 189 | //this.videobin.sync_state_with_parent (); 190 | //this.play (true); 191 | this.preload (); 192 | if (!noplay) this.seek (0, false); 193 | }, 194 | 195 | preload: function () { 196 | if (this.audio_stream) this.audiobin.set_state (Gst.State.READY); 197 | if (this.video_stream) this.videobin.set_state (Gst.State.READY); 198 | }, 199 | 200 | play: function (on_ready) { 201 | if (on_ready && this.audio_stream && this.video_stream) { 202 | this.play_on_ready = true; 203 | this.pause (); 204 | return; 205 | } 206 | if (this.video_stream) this.videobin.set_state (Gst.State.PLAYING); 207 | if (this.audio_stream) this.audiobin.set_state (Gst.State.PLAYING); 208 | this.play_on_ready = false; 209 | }, 210 | 211 | pause: function () { 212 | if (this.audio_stream) this.audiobin.set_state (Gst.State.PAUSED); 213 | if (this.video_stream) this.videobin.set_state (Gst.State.PAUSED); 214 | }, 215 | 216 | stop: function () { 217 | this.seek (0, true); 218 | if (this.audio_stream) this.audiobin.set_state (Gst.State.NULL); 219 | if (this.video_stream) this.videobin.set_state (Gst.State.NULL); 220 | }, 221 | 222 | seek: function (pos, accurate) { 223 | if (!this.audiobin || this.seek_lock) return false; 224 | this.seek_lock = true; 225 | let flag = Gst.SeekFlags.FLUSH, res; 226 | if (accurate) flag |= Gst.SeekFlags.ACCURATE; 227 | this.pause (); 228 | if (this.video_stream) res = this.videobin.seek (1.0, 229 | Gst.Format.TIME, flag, 230 | Gst.SeekType.SET, pos * Gst.MSECOND, 231 | Gst.SeekType.NONE, 0 232 | ); 233 | if (this.audio_stream) res = this.audiobin.seek (1.0, 234 | Gst.Format.TIME, flag, 235 | Gst.SeekType.SET, pos * Gst.MSECOND, 236 | Gst.SeekType.NONE, 0 237 | ); 238 | this.play (true); 239 | if (this.video_stream) this.videodec.sync_state_with_parent (); 240 | if (this.audio_stream) this.audiodec.sync_state_with_parent (); 241 | this.seek_lock = false; 242 | return res; 243 | }, 244 | 245 | set_window: function (xid) { 246 | if (!xid) return; 247 | this.handler = xid; 248 | //this.videosink.expose (); 249 | //print ("XID: ", xid); 250 | }, 251 | 252 | set_videosink: function (sink) { 253 | //this.videosink = sink; 254 | //this.playbin.set_property("video-sink", this.videosink); 255 | if (this.videosink) this.videobin.remove (this.videosink); 256 | this.videosink = sink; 257 | this.videobin.add (this.videosink); 258 | }, 259 | 260 | get_videosink: function (name) { 261 | name = name || "cluttersink"; 262 | if (!this.videosink) { 263 | this.videosink = Gst.ElementFactory.make (name, "videosink"); 264 | this.videobin.add (this.videosink); 265 | } 266 | return this.videosink; 267 | }, 268 | 269 | on_bus_message_video: function (bus, msg) { 270 | if (GstVideo.is_video_overlay_prepare_window_handle_message (msg)) { 271 | debug ("Seet overlay... " + this.handler); 272 | var overlay = msg.src; 273 | if (!overlay || !this.handler) return false; 274 | overlay.set_window_handle (this.handler); 275 | } else if (msg.type == Gst.MessageType.EOS) { 276 | this.emit ('state-changed', this.state, Gst.State.READY, 0); 277 | if (this.repeat) { 278 | this.seek (0, false); 279 | } else this.videobin.set_state (Gst.State.READY); 280 | } else if (msg.type == Gst.MessageType.STATE_CHANGED) { 281 | let [oldstate, newstate, pending] = msg.parse_state_changed (); 282 | if (this.video_state == newstate) return true; 283 | this.video_state = newstate; 284 | debug ("video state: " + newstate); 285 | this.emit ('state-changed', oldstate, newstate, pending); 286 | } else if (msg.type == Gst.MessageType.BUFFERING) { 287 | this.video_buffer = msg.parse_buffering (); 288 | this.emit ("buffering", this.video_buffer); 289 | if (this.video_buffer % 10 == 0) debug ("BUFFERING VIDEO: " + this.video_buffer); 290 | if (this.play_on_ready && this.video_buffer == this.audio_buffer && this.video_buffer == 100) { 291 | this.play (); 292 | } 293 | } else if (msg.type == Gst.MessageType.TAG) { 294 | //TODO: video tags 295 | } else if (msg.type == Gst.MessageType.ERROR) { 296 | let [err, d] = msg.parse_error (); 297 | error ("GST VIDEO ERROR: " + msg.src + "\n" + err.message); 298 | } else debug ("GST message: " + msg.type); 299 | return true; 300 | }, 301 | 302 | on_bus_message_audio: function (bus, msg) { 303 | //TODO Process messages 304 | if (msg.type == Gst.MessageType.EOS) { 305 | this.emit ('state-changed', this.state, Gst.State.READY, 0); 306 | if (this.repeat) { 307 | this.seek (0, false); 308 | } else this.audiobin.set_state (Gst.State.READY); 309 | } else if (msg.type == Gst.MessageType.STATE_CHANGED) { 310 | let [oldstate, newstate, pending] = msg.parse_state_changed (); 311 | if (this.audio_state == newstate) return true; 312 | this.audio_state = newstate; 313 | debug ("audio state: " + newstate); 314 | this.emit ('state-changed', oldstate, newstate, pending); 315 | } else if (msg.type == Gst.MessageType.BUFFERING) { 316 | this.audio_buffer = msg.parse_buffering (); 317 | this.emit ("buffering", this.audio_buffer); 318 | if (this.audio_buffer % 10 == 0) debug ("BUFFERING AUDIO: " + this.audio_buffer); 319 | if (this.play_on_ready && this.video_buffer == this.audio_buffer && this.video_buffer == 100) { 320 | this.play (); 321 | } 322 | } else if (msg.type == Gst.MessageType.TAG) { 323 | //TODO: audio tags 324 | } else if (msg.type == Gst.MessageType.ERROR) { 325 | let [err, d] = msg.parse_error (); 326 | error ("GST AUDIO ERROR: " + msg.src + "\n" + err.message); 327 | } else debug ("GST message: " + msg.type); 328 | return true; 329 | }, 330 | 331 | on_timer: function () { 332 | var pos = this.position, dur = this.duration; 333 | if (pos >= 0) { 334 | this.emit ("progress", pos, dur); 335 | //print ("progress", pos, dur); 336 | } 337 | return true; 338 | } 339 | }); 340 | 341 | Signals.addSignalMethods(PlayerEngine.prototype); 342 | 343 | const DOMAIN = "PlayerEngine"; 344 | function error (msg) {Logger.error (DOMAIN, msg)} 345 | function debug (msg) {Logger.debug (DOMAIN, msg)} 346 | function info (msg) {Logger.info (DOMAIN, msg)} 347 | -------------------------------------------------------------------------------- /common/Preferences.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gio = imports.gi.Gio; 12 | const Gtk = imports.gi.Gtk; 13 | const Lang = imports.lang; 14 | 15 | const Gettext = imports.gettext.domain ("io.github.konkor.newstream"); 16 | const _ = Gettext.gettext; 17 | 18 | //const ResultView = imports.common.ResultView; 19 | 20 | const video_format = ["vp9","avc","av01"]; 21 | const video_quality = [144,240,360,480,720,1080,1440,2160,4320]; 22 | 23 | var Preferences = new Lang.Class({ 24 | Name: "Preferences", 25 | Extends: Gtk.ScrolledWindow, 26 | 27 | _init: function (settings) { 28 | this.parent (); 29 | this.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC; 30 | this.shadow_type = Gtk.ShadowType.NONE; 31 | this.settings = settings; 32 | 33 | this.build (); 34 | }, 35 | 36 | build: function () { 37 | let label, hbox, box = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL, margin:16}); 38 | //box.margin_start = box.margin_end = 48; 39 | hbox = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL, margin:16}); 40 | this.add (hbox); 41 | hbox.pack_start (new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL}), true, true, 0); 42 | hbox.pack_start (box, true, true, 0); 43 | hbox.pack_start (new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL}), true, true, 0); 44 | 45 | label = new Gtk.Label ({ 46 | label: "" + _("General") + "",use_markup:true,xalign:0,margin_top:12}); 47 | box.add (label); 48 | 49 | box.add (new Gtk.Label ({label: "YouTube Data API Key v3", use_markup:true, xalign:0, margin_top:24})); 50 | this.warn = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL, margin:16}); 51 | box.add (this.warn); 52 | this.warn.add (new Gtk.Label ({label: "" + _("Please, Create and use your own development API key!") + "", use_markup:true, xalign:0, margin_top:12})); 53 | this.warn.add (new Gtk.Label ({ 54 | label: _("It is free with the limitation 10 000 quotas per day. Otherwise you won\'t be able to use a lot of searching on the demo key.") + 55 | "\n" + _("Find more how to create API key...")+"", 56 | use_markup:true, wrap:true, xalign:0, margin_top:12 57 | })); 58 | this.entry = new Gtk.Entry (); 59 | this.entry.placeholder_text = "YouTube API Key"; 60 | this.entry.tooltip_text = _("Enter YouTube API Key v3 here."); 61 | box.pack_start (this.entry, false, false, 12); 62 | this.entry.connect ("changed", (o) => { 63 | var s = o.text.trim(); 64 | this.warn.visible = !s; 65 | this.settings.api_key = s; 66 | }); 67 | this.entry.connect ("focus-in-event", () => { 68 | let app = Gio.Application.get_default(); 69 | app.disable_global_actions (); 70 | }); 71 | this.entry.connect ("focus-out-event", () => { 72 | let app = Gio.Application.get_default(); 73 | app.enable_global_actions (); 74 | }); 75 | 76 | box.add (new Gtk.Label ({label: "" + _("Video Preferences") + "", use_markup:true, xalign:0, margin_top:32})); 77 | 78 | hbox = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL, margin_top:24}); 79 | box.add (hbox); 80 | hbox.add (new Gtk.Label ({label: _("Preferred Video Quality")})); 81 | this.combo = new Gtk.ComboBoxText (); 82 | var id = 0, i = 0; 83 | video_quality.forEach (s => { 84 | this.combo.append_text (s.toString()+ "p"); 85 | if (s == this.settings.video_quality) id = i; 86 | i++; 87 | }); 88 | this.combo.active = id; 89 | hbox.pack_end (this.combo, false, false, 0); 90 | this.combo.connect ("changed", (o) => { 91 | this.settings.video_quality = video_quality[o.active]; 92 | }); 93 | 94 | hbox = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL, margin_top:24}); 95 | box.add (hbox); 96 | hbox.add (new Gtk.Label ({label: _("Preferred Video Format")})); 97 | this.combo_format = new Gtk.ComboBoxText (); 98 | id = 0, i = 0; 99 | video_format.forEach (s => { 100 | this.combo_format.append_text (s); 101 | if (s == this.settings.video_format) id = i; 102 | i++; 103 | }); 104 | this.combo_format.active = id; 105 | hbox.pack_end (this.combo_format, false, false, 0); 106 | this.combo_format.connect ("changed", (o) => { 107 | this.settings.video_format = video_format[o.active]; 108 | }); 109 | 110 | }, 111 | 112 | update: function () { 113 | //TODO: update settings values 114 | this.entry.text = this.settings.api_key; 115 | } 116 | }); 117 | -------------------------------------------------------------------------------- /common/ResultView.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Lang = imports.lang; 12 | const GObject = imports.gi.GObject; 13 | const GLib = imports.gi.GLib; 14 | const Gio = imports.gi.Gio; 15 | const Gtk = imports.gi.Gtk; 16 | const Gdk = imports.gi.Gdk; 17 | const GdkPixbuf = imports.gi.GdkPixbuf; 18 | 19 | const Logger = imports.common.Logger; 20 | const Utils = imports.common.Utils; 21 | 22 | let APPDIR = ""; 23 | 24 | var ResultView = new Lang.Class({ 25 | Name: "ResultView", 26 | Extends: Gtk.Box, 27 | Signals: { 28 | 'ready': { 29 | flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED}, 30 | }, 31 | 32 | _init: function (parent, scroll) { 33 | this.parent ({orientation:Gtk.Orientation.VERTICAL}); 34 | scroll = (typeof scroll !== 'undefined') ? scroll : true; 35 | let box = null; 36 | this.w = parent; 37 | APPDIR = this.w.application.current_dir; 38 | this.provider = parent.provider; 39 | 40 | this.header = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL}); 41 | this.add (this.header); 42 | 43 | if (scroll) { 44 | this.scroll = new Gtk.ScrolledWindow (); 45 | this.scroll.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC; 46 | this.scroll.shadow_type = Gtk.ShadowType.NONE; 47 | this.pack_start (this.scroll, true, true, 0); 48 | 49 | box = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL, margin:8}); 50 | this.scroll.add (box); 51 | } else box = this; 52 | 53 | let space = new Gtk.Box (); 54 | box.pack_start (space, true, false, 0); 55 | 56 | let results_box = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL, valign:0}); 57 | results_box.valign = Gtk.Align.START; 58 | box.pack_start (results_box, true, false, 0); 59 | 60 | this.results = new Gtk.FlowBox ({ 61 | homogeneous: true, 62 | activate_on_single_click: false, 63 | max_children_per_line: 3, 64 | valign: Gtk.Align.START 65 | }); 66 | this.results.can_focus = true; 67 | this.results.events |= Gdk.EventMask.FOCUS_CHANGE_MASK; 68 | results_box.pack_start (this.results, true, false, 0); 69 | 70 | this.pager = new Pager (); 71 | this.add (this.pager); 72 | 73 | space = new Gtk.Box (); 74 | box.pack_start (space, true, false, 0); 75 | 76 | this.results.connect ("child-activated", this.on_item_activated.bind (this)); 77 | this.pager.connect ("page-selected", (o, token) => { 78 | this.on_page_selected (o, token); 79 | }); 80 | this.results.connect ('key_release_event', (o, e) => { 81 | let state = o.get_selected_children ().length < 1; 82 | let app = Gio.Application.get_default(); 83 | app.lookup_action ("seek-forward").set_enabled (state); 84 | app.lookup_action ("seek-backward").set_enabled (state); 85 | app.lookup_action ("volume-up").set_enabled (state); 86 | app.lookup_action ("volume-down").set_enabled (state); 87 | }); 88 | this.results.connect ('key_press_event', (o, e) => { 89 | this.enable_global_actions (); 90 | }); 91 | this.results.connect ('leave_notify_event', (o, e) => { 92 | this.enable_global_actions (); 93 | }); 94 | }, 95 | 96 | enable_global_actions: function () { 97 | let app = Gio.Application.get_default(); 98 | app.lookup_action ("seek-forward").set_enabled (true); 99 | app.lookup_action ("seek-backward").set_enabled (true); 100 | app.lookup_action ("volume-up").set_enabled (true); 101 | app.lookup_action ("volume-down").set_enabled (true); 102 | }, 103 | 104 | query: function (words) { 105 | this.url = this.provider.get (words, this.on_results.bind (this)); 106 | }, 107 | 108 | on_results: function (data, res) { 109 | if (res != 200) return; 110 | this.emit ('ready'); 111 | this.clear_all (); 112 | try { 113 | let items = JSON.parse (Utils.bytesToString (data).toString ()); 114 | this.add_items (items); 115 | } catch (e) { debug (e.message);} 116 | if (this.scroll) this.scroll.vadjustment.value = 0; 117 | }, 118 | 119 | on_page_selected: function (o, token) { 120 | this.provider.get_page (this.url, token, this.etag, this.on_results.bind (this)); 121 | }, 122 | 123 | on_item_activated: function (o, item) { 124 | var child = item.get_children()[0].details; 125 | if (child && child.data) { 126 | this.w.itemview.load (child.data); 127 | if (this.w.stack.visible_child_name != "item") 128 | this.w.back.last = this.w.stack.visible_child_name; 129 | this.w.stack.visible_child_name = "item"; 130 | } 131 | }, 132 | 133 | add_items: function (respond) { 134 | if (respond.prevPageToken) this.pager.prev.token = respond.prevPageToken; 135 | else this.pager.prev.token = ""; 136 | if (respond.nextPageToken) this.pager.next.token = respond.nextPageToken; 137 | else this.pager.next.token = ""; 138 | if (respond.etag) this.etag = respond.etag; 139 | this.pager.toggle (); 140 | respond.items.forEach (p => { 141 | let item = new ResultViewItem (p); 142 | this.results.add (item); 143 | if (item.details.id) this.provider.get_info (item.details.id, (d) => { 144 | if (d.pageInfo && d.pageInfo.totalResults > 0) { 145 | item.details.parse (d.items[0]); 146 | item.show_details (); 147 | } else debug ("WARNING: failed detailed info for " + item.details.id + "\nRecived: " + JSON.stringify (d)); 148 | }); 149 | }); 150 | }, 151 | 152 | clear_all: function () { 153 | this.results.get_children().forEach (p => { 154 | this.results.remove (p); 155 | }); 156 | }, 157 | 158 | get first_item_data () { 159 | let child = this.results.get_children()[0]; 160 | if (child) { 161 | child = child.get_children()[0].details; 162 | if (child && child.data) { 163 | child = child.data; 164 | } 165 | } 166 | return child; 167 | } 168 | }); 169 | 170 | var ResultViewItem = new Lang.Class({ 171 | Name: "ResultViewItem", 172 | Extends: Gtk.Box, 173 | 174 | _init: function (item) { 175 | this.parent ({orientation:Gtk.Orientation.HORIZONTAL, margin:8, spacing:8}); 176 | //this.get_style_context ().add_class ("sb"); 177 | this.hexpand = false; 178 | 179 | this.details = new Details (item); 180 | this.tooltip_text = this.details.title; 181 | 182 | this.image = Gtk.Image.new_from_file (APPDIR + "/data/icons/newstream.item.svg"); 183 | this.add (this.image); 184 | let box = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL}); 185 | //box.get_style_context ().add_class ("sb"); 186 | this.pack_start (box, true, true, 8); 187 | 188 | this.title = new Gtk.Label ({ 189 | label:this.details.title, xalign:0, wrap: true, lines: 2, ellipsize: 3 190 | }); 191 | this.title.max_width_chars = 24; 192 | box.pack_start (this.title, false, false, 0); 193 | 194 | this.cbox = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL}); 195 | box.pack_start (this.cbox, true, true, 0); 196 | 197 | this.channel = new Gtk.Label ({label:this.details.channel_title, xalign:0, opacity: 0.7}); 198 | this.channel.get_style_context ().add_class ("small"); 199 | this.cbox.pack_start (this.channel, true, true, 0); 200 | 201 | this.dbox = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL}); 202 | box.pack_start (this.dbox, true, true, 0); 203 | 204 | this.published = new Gtk.Label ({label:this.details.age, xalign:0, opacity: 0.7}); 205 | this.published.get_style_context ().add_class ("small"); 206 | this.dbox.pack_start (this.published, true, true, 0); 207 | 208 | this.views = new Gtk.Label ({xalign:1, opacity: 0.7}); 209 | this.views.get_style_context ().add_class ("small"); 210 | this.dbox.pack_end (this.views, false, false, 0); 211 | 212 | this.get_thumb (); 213 | 214 | this.show_all (); 215 | }, 216 | 217 | show_details: function () { 218 | if (this.details.data.views) this.views.set_text (Utils.format_size (this.details.data.views) + " views"); 219 | if (this.details.live) 220 | this.published.set_text ("LIVE • " + this.published.get_text()); 221 | else if (this.details.duration) { 222 | this.published.set_text (Utils.time_stamp (this.details.duration) + " • " + this.published.get_text()); 223 | } 224 | this.get_channel_info (); 225 | this.details.set_cover_url (); 226 | }, 227 | 228 | get_thumb: function () { 229 | let url = this.details.get_thumbnail_url ("default"); 230 | if (url) Utils.fetch (url, null, null, (d,r) => { 231 | if (r != 200) return; 232 | //print (d.get_size(),d.get_data().length); 233 | this.image.pixbuf = GdkPixbuf.Pixbuf.new_from_stream (Gio.MemoryInputStream.new_from_bytes (d), null); 234 | }); 235 | }, 236 | 237 | get_channel_info: function () { 238 | let w = Gio.Application.get_default ().window; 239 | if (!this.details.data.channel.id) return; 240 | if (this.details.data.channel.thumbnails) this.get_channel_logo_url (); 241 | else if (w) w.provider.get_channel_info (this.details.data.channel.id, (d) => { 242 | if (d.pageInfo && d.pageInfo.resultsPerPage > 0) { 243 | this.details.parse (d.items[0]); 244 | this.get_channel_logo_url (); 245 | } else debug ("ResultViewItem.get_channel_info wrong data:\n" + JSON.stringify(d)); 246 | }); 247 | }, 248 | 249 | get_channel_logo_url: function () { 250 | let url = this.details.get_channel_thumb_url ("default"); 251 | if (url) this.details.data.channel_thumb_url = url; 252 | } 253 | }); 254 | 255 | var Details = new Lang.Class({ 256 | Name: "Details", 257 | 258 | _init: function (search_result) { 259 | if (search_result && search_result.local) 260 | this.data = search_result; 261 | else { 262 | this.data = {kind:"", id:"", channel:{}, live:false, duration:0}; 263 | this.parse (search_result); 264 | } 265 | }, 266 | 267 | get id () { 268 | return this.data.id; 269 | }, 270 | 271 | get title () { 272 | if (this.data.title) return this.data.title; 273 | else return ""; 274 | }, 275 | 276 | get age () { 277 | if (!this.data.published) return ""; 278 | return Utils.age (new Date (this.data.published)); 279 | }, 280 | 281 | get channel_title () { 282 | var channel = this.data.channel; 283 | let s = ""; 284 | if (channel && channel.title) s = channel.title; 285 | return s; 286 | }, 287 | 288 | get channel_id () { 289 | var channel = this.data.channel; 290 | let s = ""; 291 | if (channel && channel.id) s = channel.id; 292 | return s; 293 | }, 294 | 295 | get live () { 296 | return this.data.live || false; 297 | }, 298 | 299 | get duration () { 300 | return this.data.duration; 301 | }, 302 | 303 | set_cover_url: function () { 304 | let url = this.get_thumbnail_url ("maxres"); 305 | if (!url) url = this.get_thumbnail_url ("standard"); 306 | if (!url) url = this.get_thumbnail_url ("high"); 307 | if (!url) url = this.get_thumbnail_url ("medium"); 308 | if (!url) url = this.get_thumbnail_url ("default"); 309 | this.data.cover_url = url; 310 | }, 311 | 312 | get_thumbnail_url: function (preset) { 313 | let s = ""; 314 | if (!this.data.thumbnails) return s; 315 | preset = preset || "default"; 316 | var p = this.data.thumbnails[preset]; 317 | if (p && p.url) s = p.url; 318 | return s; 319 | }, 320 | 321 | get_channel_thumb_url: function (preset) { 322 | let s = ""; 323 | if (!this.data.channel.thumbnails) return s; 324 | preset = preset || "default"; 325 | var p = this.data.channel.thumbnails[preset]; 326 | if (p && p.url) s = p.url; 327 | return s; 328 | }, 329 | 330 | parse: function (data) { 331 | if (!data) return; 332 | //print (JSON.stringify (search_result)); 333 | if (data.kind == "youtube#searchResult") this.parse_search (data); 334 | else if (data.kind == "youtube#video") this.parse_search (data); 335 | else if (data.kind == "youtube#channel") this.parse_channel (data); 336 | }, 337 | 338 | parse_search: function (data) { 339 | if (!data.id) return; 340 | if (data.id.kind) this.data.kind = data.id.kind; 341 | if (data.id.videoId) this.data.id = data.id.videoId; 342 | if (data.snippet) this.parse_snippet (data.snippet); 343 | if (data.contentDetails) this.parse_content (data.contentDetails); 344 | if (data.statistics) this.parse_statistics (data.statistics); 345 | }, 346 | 347 | parse_snippet: function (snippet) { 348 | if (snippet.liveBroadcastContent && snippet.liveBroadcastContent != "none") this.data.live = true; 349 | if (snippet.title) this.data.title = snippet.title; 350 | if (snippet.publishedAt) this.data.published = snippet.publishedAt; 351 | if (snippet.description) this.data.description = snippet.description; 352 | if (snippet.channelId) this.data.channel.id = snippet.channelId; 353 | if (snippet.channelTitle) this.data.channel.title = snippet.channelTitle; 354 | // "name":{url:"",width:n,height:n} 355 | if (snippet.thumbnails) this.data.thumbnails = snippet.thumbnails; 356 | if (snippet.tags) this.data.tags = snippet.tags; 357 | if (snippet.categoryId) this.data.category_id = snippet.categoryId; 358 | }, 359 | 360 | parse_channel: function (item) { 361 | if (item.snippet.title) this.data.channel.title = item.snippet.title; 362 | if (item.snippet.publishedAt) this.data.channel.published = item.snippet.publishedAt; 363 | if (item.snippet.description) this.data.channel.description = item.snippet.description; 364 | if (item.snippet.thumbnails) this.data.channel.thumbnails = item.snippet.thumbnails; 365 | if (!item.statistics) return; 366 | if (item.statistics.viewCount) this.data.channel.views = item.statistics.viewCount; 367 | if (item.statistics.subscriberCount) this.data.channel.subscribers = item.statistics.subscriberCount; 368 | if (item.statistics.videoCount) this.data.channel.videos = item.statistics.videoCount; 369 | }, 370 | 371 | parse_content: function (data) { 372 | if (data.duration) this.data.duration = this.parse_duration (data.duration); 373 | // dimension, definition, caption, licensedContent, projection 374 | }, 375 | 376 | parse_duration: function (data) { 377 | let s = data.substring (2); 378 | let h = 0, m = 0, sec = 0, i; 379 | if (s.length < 3) return 0; 380 | i = s.indexOf ("H"); 381 | if (i > -1) { 382 | h = parseInt(s.substring (0,i)); 383 | if (!Number.isInteger (h)) h = 0; 384 | s = s.substring (i + 1); 385 | } 386 | i = s.indexOf ("M"); 387 | if (i > -1) { 388 | m = parseInt(s.substring (0,i)); 389 | if (!Number.isInteger (m)) m = 0; 390 | s = s.substring (i + 1); 391 | } 392 | i = s.indexOf ("S"); 393 | if (i > -1) { 394 | sec = parseInt(s.substring (0,i)); 395 | if (!Number.isInteger (sec)) sec = 0; 396 | } 397 | return h*3600 + m*60 + sec; 398 | }, 399 | 400 | parse_statistics: function (data) { 401 | if (data.viewCount) this.data.views = data.viewCount; 402 | if (data.likeCount) this.data.likes = data.likeCount; 403 | if (data.dislikeCount) this.data.dislikes = data.dislikeCount; 404 | // favoriteCount, commentCount 405 | } 406 | }); 407 | 408 | var Pager = new Lang.Class({ 409 | Name: "Pager", 410 | Extends: Gtk.Box, 411 | Signals: { 412 | 'page-selected': { 413 | flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED, 414 | param_types: [GObject.TYPE_STRING]}, 415 | }, 416 | 417 | _init: function () { 418 | this.parent ({orientation:Gtk.Orientation.HORIZONTAL, margin:8}); 419 | //this.get_style_context ().add_class ("sb"); 420 | 421 | let space = new Gtk.Box (); 422 | this.pack_start (space, true, false, 0); 423 | 424 | this.first = this.add_button ("First", "First page"); 425 | this.prev = this.add_button ("Previous", "Previous page"); 426 | this.next = this.add_button ("Next", "Next page"); 427 | 428 | space = new Gtk.Box (); 429 | this.pack_start (space, true, false, 0); 430 | 431 | this.current = this.first; 432 | this.show_all (); 433 | //this.first.visible = true; 434 | }, 435 | 436 | add_button: function (label, tooltip) { 437 | let btn = new Gtk.Button ({label:label, tooltip_text:tooltip}); 438 | //btn.get_style_context ().add_class ("sb-button"); 439 | btn.token = ""; 440 | btn.no_show_all = true; 441 | this.pack_start (btn, false, false, 8); 442 | 443 | btn.connect ('clicked', this.on_clicked.bind (this)); 444 | 445 | return btn; 446 | }, 447 | 448 | on_clicked: function (o) { 449 | this.emit ('page_selected', o.token); 450 | }, 451 | 452 | toggle: function () { 453 | this.first.visible = !!this.prev.token; 454 | this.prev.visible = !!this.prev.token; 455 | this.next.visible = !!this.next.token; 456 | } 457 | }); 458 | 459 | const DOMAIN = "ResultView"; 460 | function error (msg) {Logger.error (DOMAIN, msg)} 461 | function debug (msg) {Logger.debug (DOMAIN, msg)} 462 | function info (msg) {Logger.info (DOMAIN, msg)} 463 | -------------------------------------------------------------------------------- /common/Search.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Lang = imports.lang; 12 | const GObject = imports.gi.GObject; 13 | const GLib = imports.gi.GLib; 14 | const Gio = imports.gi.Gio; 15 | const Gdk = imports.gi.Gdk; 16 | const Gtk = imports.gi.Gtk; 17 | 18 | let APPDIR = ""; 19 | 20 | var Searchbar = new Lang.Class({ 21 | Name: "Searchbar", 22 | Extends: Gtk.Box, 23 | 24 | _init: function (parent) { 25 | this.parent ({orientation:Gtk.Orientation.VERTICAL}); 26 | this.settings = parent.settings; 27 | APPDIR = parent.application.current_dir; 28 | this.get_style_context ().add_class ("search-bar"); 29 | 30 | let box = new Gtk.Box ({orientation:Gtk.Orientation.HORIZONTAL}); 31 | box.margin = 8; 32 | this.pack_start (box, true, true, 0); 33 | 34 | let space = new Gtk.Box (); 35 | box.pack_start (space, true, false, 0); 36 | 37 | this.search_button = new Gtk.Button ({always_show_image: true, tooltip_text:"Search"}); 38 | this.search_button.image = Gtk.Image.new_from_file (APPDIR + "/data/icons/folder-saved-search-symbolic.svg"); 39 | this.search_button.get_style_context ().add_class ("hb-button"); 40 | box.pack_start (this.search_button, false, false, 8); 41 | 42 | this.entry = new Gtk.Entry (); 43 | this.entry.get_style_context ().add_class ("search-entry"); 44 | this.entry.input_hints = Gtk.InputHints.SPELLCHECK | Gtk.InputHints.WORD_COMPLETION; 45 | this.entry.placeholder_text = "Search"; 46 | box.pack_start (this.entry, true, true, 0); 47 | 48 | this.clear_button = new Gtk.Button ({always_show_image: true, tooltip_text:"Clear"}); 49 | this.clear_button.image = Gtk.Image.new_from_file (APPDIR + "/data/icons/window-close-symbolic.svg"); 50 | this.clear_button.get_style_context ().add_class ("hb-button"); 51 | box.pack_start (this.clear_button, false, false, 8); 52 | 53 | space = new Gtk.Box (); 54 | box.pack_start (space, true, false, 0); 55 | 56 | this.history = new SearchHistory (this); 57 | this.add (this.history); 58 | 59 | this.clear_button.connect ('clicked', () => { 60 | this.entry.text = ""; 61 | }); 62 | this.entry.connect ('key_press_event', (o, e) => { 63 | var [,key] = e.get_keyval (); 64 | if (key == Gdk.KEY_Escape) this.entry.text = ""; 65 | }); 66 | this.entry.connect ('activate', () => { 67 | this.search_button.clicked (); 68 | }); 69 | this.entry.connect ('notify::text', (o, a) => { 70 | this.history.update (); 71 | }); 72 | this.entry.connect ('focus-in-event', (o, e) => { 73 | let app = Gio.Application.get_default(); 74 | app.disable_global_actions (); 75 | this.history.visible = true; 76 | }); 77 | this.entry.connect ('focus-out-event', (o, e) => { 78 | //this.history.visible = false; 79 | let app = Gio.Application.get_default(); 80 | app.enable_global_actions (); 81 | GLib.timeout_add (0, 200, () => { 82 | this.history.visible = false; 83 | return false; 84 | }); 85 | }); 86 | this.history.connect ('selected', (o, t) => { 87 | //print ("selected", t); 88 | this.entry.text = t; 89 | this.search_button.clicked (); 90 | }); 91 | } 92 | }); 93 | 94 | var SearchHistory = new Lang.Class({ 95 | Name: "SearchHistory", 96 | Extends: Gtk.Box, 97 | Signals: { 98 | 'selected': { 99 | flags: GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.DETAILED, 100 | param_types: [GObject.TYPE_STRING]}, 101 | }, 102 | 103 | _init: function (bar) { 104 | this.parent ({orientation:Gtk.Orientation.VERTICAL, spacing:8}); 105 | this.get_style_context ().add_class ("search-bar"); 106 | this.no_show_all = true; 107 | this.bar = bar; 108 | 109 | this.items = []; 110 | 111 | this.items.push (new SearchHistoryItem ()); 112 | this.items.push (new SearchHistoryItem ()); 113 | this.items.push (new SearchHistoryItem ()); 114 | this.items.forEach (p => { 115 | this.add (p); 116 | p.connect ("clicked", (o) => { 117 | this.emit ("selected", o.get_label ()); 118 | }); 119 | }); 120 | 121 | this.connect ("map", this.update.bind (this)); 122 | }, 123 | 124 | update: function (o, e) { 125 | let history = this.get_history (); 126 | for (let i = 0; i < 3; i++) { 127 | if (history[i]) this.items[i].set_text (history[i]); 128 | else this.items[i].set_text (""); 129 | } 130 | }, 131 | 132 | get_history: function () { 133 | var filter = this.bar.entry.text.trim().toLowerCase(); 134 | var sh = this.bar.settings.history; 135 | let history = []; 136 | for (let i = 0; i < sh.length; i++) { 137 | if (filter) { 138 | if (sh[i].toLowerCase().indexOf (filter) > -1) 139 | history.push (sh[i]); 140 | } else history.push (sh[i]); 141 | if (history.length > 2) break; 142 | } 143 | return history; 144 | } 145 | 146 | }); 147 | 148 | var SearchHistoryItem = new Lang.Class({ 149 | Name: "SearchHistoryItem", 150 | Extends: Gtk.Button, 151 | 152 | _init: function (text) { 153 | this.parent ({always_show_image: true, tooltip_text:"Search History", xalign:0}); 154 | //this.get_style_context ().add_class ("search-bar"); 155 | this.set_relief (Gtk.ReliefStyle.NONE); 156 | 157 | this.image = Gtk.Image.new_from_file (APPDIR + "/data/icons/history-symbolic.svg"); 158 | var l = this.get_image (); 159 | l.margin_right = 12; 160 | l.margin_left = 64; 161 | 162 | //wrap: true, lines: 1, ellipsize: 3, xalign:0}); 163 | //this.width_chars = 12; 164 | this.show_all (); 165 | this.set_text (text); 166 | }, 167 | 168 | set_text: function (text) { 169 | text = text || ""; 170 | this.set_label (text.trim()); 171 | this.visible = this.get_label().length != 0; 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /common/SearchProvider.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gio = imports.gi.Gio; 12 | const GLib = imports.gi.GLib; 13 | const Lang = imports.lang; 14 | const Format = imports.format; 15 | 16 | String.prototype.format = Format.format; 17 | 18 | const fetch = imports.common.Utils.fetch; 19 | const bytesToString = imports.common.Utils.bytesToString; 20 | const cache_dir = GLib.build_filenamev ([GLib.get_user_data_dir(),"newstream","cache"]); 21 | 22 | const ORDER = { 23 | 0: "date", 24 | 1: "rating", 25 | 2: "title", 26 | 3: "videoCount", 27 | 4: "viewCount", 28 | } 29 | const TIME = { 30 | 0: "last_24_hours", 31 | 1: "last_7_days", 32 | 2: "last_30_days", 33 | 3: "last_1_year", 34 | 4: "all_time" 35 | } 36 | const SAFESEARCH = { 37 | 0: "moderate", 38 | 1: "none", 39 | 2: "strict" 40 | } 41 | const VIDEOCAPTION = { 42 | 0: "any", 43 | 1: "closedCaption", 44 | 2: "none" 45 | } 46 | const VIDEODEFINITION = { 47 | 0: "any", 48 | 1: "high", 49 | 2: "standard" 50 | } 51 | const VIDEODIMENSION = { 52 | 0: "2d", 53 | 1: "3d", 54 | 2: "any" 55 | } 56 | const VIDEODURATION = { 57 | 0: "any", 58 | 1: "long", 59 | 2: "medium", 60 | 3: "short" 61 | } 62 | const VIDEOLICENSE = { 63 | 0: "any", 64 | 1: "creativeCommon", 65 | 2: "youtube" 66 | } 67 | const VIDEOTYPE = { 68 | 0: "any", 69 | 1: "episode", 70 | 2: "movie" 71 | } 72 | 73 | const BASE_URL = 'https://www.googleapis.com/youtube/v3/'; 74 | const KEY = 'AIzaSyDEsAMmHbBAQXIdklCC6sPyoV4PtdS9D0Q'; 75 | 76 | var SearchProvider = new Lang.Class({ 77 | Name: "SearchProvider", 78 | 79 | _init: function (settings) { 80 | this.settings = settings; 81 | this._order = ORDER[0]; 82 | this._time = TIME[4]; 83 | this._safesearch = SAFESEARCH[0]; 84 | this._videocaption = VIDEOCAPTION[0]; 85 | this._videodefinition = VIDEODEFINITION[0]; 86 | this._videodimension = VIDEODIMENSION[0] 87 | this._videoduration = VIDEODURATION[0]; 88 | this._videolicense = VIDEOLICENSE[0]; 89 | this._videotype = VIDEOTYPE[0]; 90 | this._max_results = 24; 91 | }, 92 | 93 | get key () { return this.settings.api_key || KEY; }, 94 | 95 | calculate_time: function (thetime) { 96 | let search_time_string = ""; 97 | let publishedAfter = new Date(); 98 | let publishedBefore = new Date(); 99 | switch(thetime){ 100 | case 'last_24_hours': 101 | publishedAfter.setDate(publishedBefore.getDate()-1); 102 | search_time_string = '&publishedAfter=%s&publishedBefore=%s'.format( 103 | publishedAfter.toISOString(), 104 | publishedBefore.toISOString() 105 | ); 106 | break; 107 | case 'last_7_days': 108 | publishedAfter.setDate(publishedBefore.getDate()-7); 109 | search_time_string = '&publishedAfter=%s&publishedBefore=%s'.format( 110 | publishedAfter.toISOString(), 111 | publishedBefore.toISOString() 112 | ); 113 | break; 114 | case 'last_30_days': 115 | publishedAfter.setDate(publishedBefore.getDate()-30); 116 | search_time_string = '&publishedAfter=%s&publishedBefore=%s'.format( 117 | publishedAfter.toISOString(), 118 | publishedBefore.toISOString() 119 | ); 120 | break; 121 | case 'last_1_year': 122 | publishedAfter.setDate(publishedBefore.getDate()-365); 123 | search_time_string = '&publishedAfter=%s&publishedBefore=%s'.format( 124 | publishedAfter.toISOString(), 125 | publishedBefore.toISOString() 126 | ); 127 | break; 128 | } 129 | return search_time_string; 130 | }, 131 | 132 | _build_query_url: function (query) { 133 | let url = '%ssearch?part=snippet&q=%s&order=%s&maxResults=%s&type=video&safeSearch=%s&videoCaption=%s&videoDefinition=%s&videoDimension=%s&videoDuration=%s&videoLicense=%s&videoType=%s%s&key=%s'.format ( 134 | BASE_URL, 135 | encodeURIComponent (query), 136 | this._order, 137 | this._max_results, 138 | this._safesearch, 139 | this._videocaption, 140 | this._videodefinition, 141 | this._videodimension, 142 | this._videoduration, 143 | this._videolicense, 144 | this._videotype, 145 | this.calculate_time (this._time), this.key 146 | ); 147 | return url; 148 | }, 149 | 150 | get: function (query, callback) { 151 | let url = this._build_query_url (query); 152 | print (url); 153 | fetch (url, null, null, (d,r) => { 154 | //if (r != 200) print ("Search respond:", r, "\n", d); 155 | callback (d, r); 156 | }); 157 | return url; 158 | }, 159 | 160 | get_page: function (query, token, etag, callback) { 161 | let url = query; 162 | if (token) url += "&pageToken=" + token; 163 | //if (etag) url += "&etag=" + etag; 164 | fetch (url, null, null, callback); 165 | }, 166 | 167 | get_info: function (id, callback) { 168 | if (!id) return; 169 | let data, d = get_cache (id); 170 | if (d) { 171 | data = JSON.parse (bytesToString (d)); 172 | callback (data); 173 | return; 174 | } 175 | let url = '%svideos?part=snippet,contentDetails,statistics&id=%s&key=%s'.format ( 176 | BASE_URL, id, this.key 177 | ); 178 | //print (url); 179 | fetch (url, null, null, (d) => { 180 | let data; 181 | try { 182 | data = JSON.parse (bytesToString (d)); 183 | if (!data.error) set_cache (id, d); 184 | callback (data); 185 | } catch (e) { 186 | print (e.msg + "\nRecived data: " + d); 187 | } 188 | }); 189 | }, 190 | 191 | get_channel: function (id, callback) { 192 | if (!id) return ""; 193 | let url = '%ssearch?part=snippet&order=date&maxResults=%s&type=video&channelId=%s&key=%s'.format ( 194 | BASE_URL, this._max_results, id, this.key 195 | ); 196 | fetch (url, null, null, callback); 197 | return url; 198 | }, 199 | 200 | get_channel_info: function (id, callback) { 201 | if (!id) return; 202 | let data, d = get_cache (id); 203 | if (d) { 204 | data = JSON.parse (bytesToString (d)); 205 | callback (data); 206 | return; 207 | } 208 | let url = '%schannels?part=snippet,statistics&id=%s&key=%s'.format ( 209 | BASE_URL, id, this.key 210 | ); 211 | fetch (url, null, null, (d) => { 212 | let data = JSON.parse (bytesToString (d)); 213 | if (!data.error) set_cache (id, d); 214 | callback (data); 215 | }); 216 | }, 217 | 218 | get_hot: function (callback) { 219 | let url = '%ssearch?part=snippet&order=viewCount&maxResults=%s&type=video%s&key=%s'.format ( 220 | BASE_URL, this._max_results, this.calculate_time ("last_7_days"), this.key 221 | ); 222 | fetch (url, null, null, callback); 223 | return url; 224 | }, 225 | 226 | get_day: function (callback) { 227 | let url = '%ssearch?part=snippet&order=date&maxResults=%s&type=video%s&key=%s'.format ( 228 | BASE_URL, this._max_results, this.calculate_time ("last_24_hours"), this.key 229 | ); 230 | fetch (url, null, null, callback); 231 | return url; 232 | }, 233 | 234 | get_hit: function (callback) { 235 | let url = '%ssearch?part=snippet&order=viewCount&maxResults=%s&type=video&key=%s'.format ( 236 | BASE_URL, this._max_results, this.key 237 | ); 238 | fetch (url, null, null, callback); 239 | return url; 240 | }, 241 | 242 | get_relaited: function (id, callback) { 243 | let url = '%ssearch?part=snippet&order=viewCount&maxResults=%s&type=video&relatedToVideoId=%s&key=%s'.format ( 244 | BASE_URL, 4, id, this.key 245 | ); 246 | fetch (url, null, null, callback); 247 | return url; 248 | } 249 | }); 250 | 251 | function set_cache (id, data) { 252 | if (!id || !data || !data.length) return; 253 | try { 254 | GLib.file_set_contents (cache_dir + "/" + id, data); 255 | } catch (e) { 256 | print (e); 257 | } 258 | } 259 | 260 | function get_cache (id) { 261 | if (!id) return null; 262 | let f = Gio.file_new_for_path (cache_dir + "/" + id); 263 | if (f.query_exists(null)) { 264 | let [res, ar, tags] = f.load_contents (null); 265 | if (res) return ar; 266 | } 267 | return null; 268 | } 269 | -------------------------------------------------------------------------------- /common/Settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Lang = imports.lang; 12 | const Gdk = imports.gi.Gdk; 13 | const GLib = imports.gi.GLib; 14 | const Gio = imports.gi.Gio; 15 | 16 | const Utils = imports.common.Utils; 17 | const Logger = imports.common.Logger; 18 | 19 | let app_data_dir = get_app_data_dir (); 20 | 21 | let save = true; 22 | let history = []; 23 | let history_size = 5000; 24 | let view_history = []; 25 | let view_history_size = 1000; 26 | let bookmarks = []; 27 | let channels = []; 28 | let tags = []; 29 | 30 | let config_id = 0; 31 | let config = { 32 | api_key: "", 33 | video_quality: 720, 34 | video_format: "webm", 35 | show_revalent: true 36 | } 37 | 38 | var Settings = new Lang.Class({ 39 | Name: "Settings", 40 | Extends: Gio.Settings, 41 | 42 | _init: function () { 43 | const schema = 'io.github.konkor.newstream'; 44 | const GioSSS = Gio.SettingsSchemaSource; 45 | 46 | let schemaDir = Gio.File.new_for_path (getCurrentFile()[1] + '/schemas'); 47 | let schemaSource; 48 | if (schemaDir.query_exists(null)) 49 | schemaSource = GioSSS.new_from_directory ( 50 | schemaDir.get_path(), 51 | GioSSS.get_default(), 52 | false 53 | ); 54 | else 55 | schemaSource = GioSSS.get_default(); 56 | 57 | let schemaObj = schemaSource.lookup(schema, true); 58 | if (!schemaObj) 59 | throw new Error('Schema ' + schema + ' could not be found. \n' + 60 | 'Please check your installation.'); 61 | this.parent ({ settings_schema: schemaObj }); 62 | this.load (); 63 | this.view_history_modified = true; 64 | }, 65 | 66 | quit: function () { 67 | if (config_id) { 68 | this.unschedule_config (); 69 | this.save_config (); 70 | } 71 | }, 72 | 73 | load: function () { 74 | save = this.get_boolean ("save-settings"); 75 | history_size = this.get_int ("history-size"); 76 | view_history_size = this.get_int ("view-history-size"); 77 | this.load_history (); 78 | this.load_tags (); 79 | this.load_view_history (); 80 | this.load_bookmarks (); 81 | this.load_channels (); 82 | this.load_config (); 83 | }, 84 | 85 | get save () { return save; }, 86 | set save (val) { 87 | save = val; 88 | this.set_boolean ("save-settings", save); 89 | }, 90 | 91 | get window_height () { return this.get_int ("window-height"); }, 92 | get window_width () { return this.get_int ("window-width"); }, 93 | get window_x () { return this.get_int ("window-x"); }, 94 | get window_y () { return this.get_int ("window-y"); }, 95 | get window_maximized () { return this.get_boolean ("window-maximized"); }, 96 | 97 | save_geometry: function (o) { 98 | let window = o.get_window (); 99 | if (!window) return; 100 | let ws = window.get_state(); 101 | let x = 0, y = 0, w = 480, h = 720, maximized = false; 102 | 103 | if (Gdk.WindowState.MAXIMIZED & ws) { 104 | maximized = true; 105 | } else if ((Gdk.WindowState.TILED & ws) == 0) { 106 | [x, y] = window.get_position (); 107 | [w, h] = o.get_size (); 108 | w = window.get_width()-65; 109 | print (w); 110 | this.set_int ("window-x", x); 111 | this.set_int ("window-y", y); 112 | this.set_int ("window-width", w); 113 | this.set_int ("window-height", h); 114 | } 115 | this.set_boolean ("window-maximized", maximized); 116 | }, 117 | 118 | load_config: function () { 119 | let o, f = Gio.file_new_for_path (app_data_dir + "/config.json"); 120 | if (f.query_exists(null)) { 121 | var [res, ar, tags] = f.load_contents (null); 122 | if (res) try { 123 | o = JSON.parse (Utils.bytesToString (ar)); 124 | for (let property in config) { 125 | if (o[property]) config[property] = o[property]; 126 | } 127 | } catch (e) {} 128 | } 129 | }, 130 | 131 | save_config: function () { 132 | GLib.file_set_contents (app_data_dir + "/config.json", JSON.stringify (config)); 133 | this.unschedule_config (); 134 | }, 135 | 136 | schedule_config: function () { 137 | this.unschedule_config (); 138 | config_id = GLib.timeout_add_seconds (100, 20, this.save_config.bind (this)); 139 | }, 140 | 141 | unschedule_config: function () { 142 | if (config_id) GLib.source_remove (config_id); 143 | config_id = 0; 144 | }, 145 | 146 | get api_key () { return config.api_key; }, 147 | set api_key (val) { 148 | if (config.api_key == val) return; 149 | config.api_key = val; 150 | this.schedule_config (); 151 | }, 152 | 153 | get video_quality () { return config.video_quality; }, 154 | set video_quality (val) { 155 | if (config.video_quality == val) return; 156 | config.video_quality = val; 157 | this.schedule_config (); 158 | }, 159 | 160 | get video_format () { return config.video_format; }, 161 | set video_format (val) { 162 | if (config.video_format == val) return; 163 | config.video_format = val; 164 | this.schedule_config (); 165 | }, 166 | 167 | get show_revalent () { return config.show_revalent; }, 168 | set show_revalent (val) { 169 | if (config.show_revalent == val) return; 170 | config.show_revalent = val; 171 | this.schedule_config (); 172 | }, 173 | 174 | get history () { return history; }, 175 | set history (val) { 176 | history = val; 177 | this.save_history (); 178 | }, 179 | 180 | get history_size () { return history_size; }, 181 | set history_size (val) { 182 | history_size = val; 183 | this.set_int ("history-size", history_size); 184 | }, 185 | 186 | history_add: function (text) { 187 | if (!text) return; 188 | let s = text.trim (); 189 | if (!s) return; 190 | 191 | var i = history.indexOf (s); 192 | if (i > -1) history.splice (i, 1); 193 | history.unshift (s); 194 | 195 | //saving history 196 | if (history_size < 1) return; 197 | if (history.length > history_size) history.pop (); 198 | this.save_history (); 199 | }, 200 | 201 | save_history: function () { 202 | GLib.file_set_contents (app_data_dir + "/history.json", JSON.stringify (history)); 203 | }, 204 | 205 | load_history: function () { 206 | let f = Gio.file_new_for_path (app_data_dir + "/history.json"); 207 | if (f.query_exists(null)) { 208 | var [res, ar, tags] = f.load_contents (null); 209 | if (res) try { 210 | history = JSON.parse (Utils.bytesToString (ar)); 211 | } catch (e) { 212 | history = []; 213 | } 214 | } 215 | }, 216 | 217 | get tags () { return tags; }, 218 | 219 | tagged: function (tag) { 220 | return tag && (tags.indexOf (tag) > -1); 221 | }, 222 | 223 | load_tags: function () { 224 | let f = Gio.file_new_for_path (app_data_dir + "/tags.json"); 225 | if (f.query_exists(null)) { 226 | var [res, ar, tags] = f.load_contents (null); 227 | if (res) try { 228 | tags = JSON.parse (Utils.bytesToString (ar)); 229 | } catch (e) { 230 | tags = []; 231 | } 232 | } 233 | }, 234 | 235 | get view_history_size () { return view_history_size; }, 236 | set view_history_size (val) { 237 | view_history_size = val; 238 | this.set_int ("view-history-size", view_history_size); 239 | }, 240 | 241 | get view_history () { return view_history; }, 242 | 243 | viewed: function (id) { 244 | return id && (view_history.indexOf (id) > -1); 245 | }, 246 | 247 | add_view_history: function (data) { 248 | if (!data || !data.id) return; 249 | let s = data.id; 250 | if (!data.local) data.local = {views: 1}; 251 | data.local.last = Date.now (); 252 | 253 | var i = view_history.indexOf (s); 254 | if (i > -1) { 255 | view_history.splice (i, 1); 256 | let it = this.get_view_history_item (s); 257 | if (it && it.local) data.local.views = it.local.views + 1; 258 | } 259 | view_history.unshift (s); 260 | 261 | //saving history 262 | if (view_history_size < 1) return; 263 | if (view_history.length > view_history_size) { 264 | s = view_history.pop (); 265 | if (s && !this.booked (s)) this.remove_data (s); 266 | } 267 | this.save_view_history (data); 268 | this.view_history_modified = true; 269 | }, 270 | 271 | get_view_history_item: function (id) { 272 | let data = null; 273 | let f = Gio.file_new_for_path (app_data_dir + "/data/" + id + ".json"); 274 | if (f.query_exists(null)) { 275 | var [res, ar, tags] = f.load_contents (null); 276 | if (res) try { 277 | data = JSON.parse (Utils.bytesToString (ar)); 278 | } catch (e) { 279 | print ("Can't load item " + app_data_dir + "/" + s + ".json ..."); 280 | } 281 | } 282 | return data; 283 | }, 284 | 285 | save_view_history: function (data) { 286 | GLib.file_set_contents (app_data_dir + "/view_history.json", JSON.stringify (view_history)); 287 | this.add_data (data); 288 | }, 289 | 290 | load_view_history: function () { 291 | let f = Gio.file_new_for_path (app_data_dir + "/view_history.json"); 292 | if (f.query_exists(null)) { 293 | var [res, ar, tags] = f.load_contents (null); 294 | if (res) try { 295 | view_history = JSON.parse (Utils.bytesToString (ar)); 296 | } catch (e) { 297 | view_history = []; 298 | } 299 | } 300 | }, 301 | 302 | add_data: function (data) { 303 | if (!data && !data.id) return; 304 | try { 305 | GLib.file_set_contents (app_data_dir + "/data/" + data.id + ".json", JSON.stringify (data)); 306 | } catch (e) { 307 | print ("Can't store " + app_data_dir + "/data/" + data.id + ".json ...\n", e); 308 | } 309 | }, 310 | 311 | remove_data: function (id) { 312 | try { 313 | Gio.File.new_for_path (app_data_dir + "/data/" + id + ".json").delete (null); 314 | } catch (e) { 315 | print ("Can't delete " + app_data_dir + "/data/" + id + ".json ...\n", e); 316 | } 317 | }, 318 | 319 | get bookmarks () { return bookmarks; }, 320 | 321 | booked: function (id) { 322 | return id && (bookmarks.indexOf (id) > -1); 323 | }, 324 | 325 | load_bookmarks: function () { 326 | let f = Gio.file_new_for_path (app_data_dir + "/bookmarks.json"); 327 | if (f.query_exists(null)) { 328 | var [res, ar, tags] = f.load_contents (null); 329 | if (res) try { 330 | bookmarks = JSON.parse (Utils.bytesToString (ar)); 331 | } catch (e) { 332 | bookmarks = []; 333 | } 334 | } 335 | this.bookmarks_modified = true; 336 | }, 337 | 338 | toggle_bookmark: function (id, state) { 339 | if (!id || (this.booked (id) && state) || (!this.booked (id) && !state)) return; 340 | if (state) bookmarks.unshift (id); 341 | else { 342 | bookmarks.splice (bookmarks.indexOf (id), 1); 343 | if (!this.viewed (id)) this.remove_data (id); 344 | } 345 | try { 346 | GLib.file_set_contents (app_data_dir + "/bookmarks.json", JSON.stringify (bookmarks)); 347 | } catch (e) { 348 | print (e); 349 | } 350 | this.bookmarks_modified = true; 351 | }, 352 | 353 | get channels () { return channels; }, 354 | 355 | subscribed: function (id) { 356 | return id && (channels.indexOf (id) > -1); 357 | }, 358 | 359 | load_channels: function () { 360 | let f = Gio.file_new_for_path (app_data_dir + "/channels.json"); 361 | if (f.query_exists(null)) { 362 | var [res, ar, tags] = f.load_contents (null); 363 | if (res) try { 364 | channels = JSON.parse (Utils.bytesToString (ar)); 365 | } catch (e) { 366 | channels = []; 367 | } 368 | } 369 | this.channels_modified = true; 370 | }, 371 | 372 | toggle_channel: function (channel, state) { 373 | let id = channel.id; 374 | if (!id || (this.subscribed (id) && state) || (!this.subscribed (id) && !state)) return; 375 | if (state) { 376 | channels.unshift (id); 377 | this.add_data (channel); 378 | } else { 379 | channels.splice (channels.indexOf (id), 1); 380 | this.remove_data (id); 381 | } 382 | try { 383 | GLib.file_set_contents (app_data_dir + "/channels.json", JSON.stringify (channels)); 384 | } catch (e) { 385 | print (e); 386 | } 387 | this.channels_modified = true; 388 | } 389 | 390 | }); 391 | 392 | function get_app_data_dir () { 393 | let path = GLib.build_filenamev ([GLib.get_user_data_dir(),"newstream"]); 394 | if (!GLib.file_test (path, GLib.FileTest.EXISTS)) 395 | GLib.mkdir_with_parents (path, 484); 396 | if (!GLib.file_test (path + "/data", GLib.FileTest.EXISTS)) 397 | GLib.mkdir_with_parents (path + "/data", 484); 398 | if (!GLib.file_test (path + "/cache", GLib.FileTest.EXISTS)) 399 | GLib.mkdir_with_parents (path + "/cache", 484); 400 | return path; 401 | } 402 | 403 | function getCurrentFile () { 404 | let stack = (new Error()).stack; 405 | let stackLine = stack.split('\n')[1]; 406 | if (!stackLine) 407 | throw new Error ('Could not find current file'); 408 | let match = new RegExp ('@(.+):\\d+').exec(stackLine); 409 | if (!match) 410 | throw new Error ('Could not find current file'); 411 | let path = match[1]; 412 | let file = Gio.File.new_for_path (path).get_parent(); 413 | return [file.get_path(), file.get_parent().get_path(), file.get_basename()]; 414 | } 415 | -------------------------------------------------------------------------------- /common/SideMenu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gtk = imports.gi.Gtk; 12 | const Gio = imports.gi.Gio; 13 | const Lang = imports.lang; 14 | 15 | var SideMenu = new Lang.Class({ 16 | Name: "SideMenu", 17 | Extends: Gtk.ScrolledWindow, 18 | 19 | _init: function () { 20 | this.parent (); 21 | this.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC; 22 | this.hscrollbar_policy = Gtk.PolicyType.NEVER; 23 | this.shadow_type = Gtk.ShadowType.NONE; 24 | 25 | this.submenus = []; 26 | 27 | this.content = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL}); 28 | this.content.get_style_context ().add_class ("side-menu"); 29 | this.add (this.content); 30 | this.propagate_natural_height = true; 31 | }, 32 | 33 | add_item: function (item) { 34 | this.content.add (item); 35 | }, 36 | 37 | add_submenu: function (item) { 38 | this.content.add (item); 39 | item.id = this.submenus.length; 40 | this.submenus.push (item); 41 | item.connect ('activate', this.on_submenu_activate.bind (this)); 42 | }, 43 | 44 | on_submenu_activate: function (item) { 45 | if (item && item.button.active) this.submenus.forEach ( p => { 46 | if (p.id != item.id) p.expanded = false; 47 | }); 48 | let h = this.content.get_preferred_height ()[0]; 49 | let max = Gio.Application.get_default ().window.window.get_height () - 96; 50 | if (h > max) h = max; 51 | //this.set_size_request (128, h); 52 | this.height_request = h; 53 | } 54 | }); 55 | 56 | var SideSubmenu = new Lang.Class({ 57 | Name: "SideSubmenu", 58 | Extends: Gtk.Box, 59 | Signals: { 60 | 'activate': {}, 61 | }, 62 | 63 | _init: function (text, info, tooltip) { 64 | this.parent ({orientation:Gtk.Orientation.VERTICAL, margin:0, spacing:0}); 65 | this.id = 0; 66 | 67 | this.info = new InfoLabel ({no_show_all:false}); 68 | this.info.label.set_text (text); 69 | this.info.info.set_text (info); 70 | this.info.info.xalign = 1; 71 | this.button = new Gtk.ToggleButton ({tooltip_text:tooltip, xalign:0}); 72 | this.button.get_style_context ().add_class ("sidesubmenu"); 73 | this.button.set_relief (Gtk.ReliefStyle.NONE); 74 | this.info.info.get_style_context ().add_class ("infolabel"); 75 | this.button.add (this.info); 76 | this.add (this.button); 77 | 78 | this.section = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL, margin:0, spacing:0}); 79 | this.section.margin_top = this.section.margin_bottom = 6; 80 | this.section.no_show_all = true; 81 | this.section.get_style_context ().add_class ("sidesection"); 82 | this.add (this.section); 83 | 84 | this.button.connect ('toggled', this.on_toggle.bind (this)); 85 | }, 86 | 87 | on_toggle: function (o) { 88 | this.section.visible = o.active; 89 | this.emit ('activate'); 90 | }, 91 | 92 | get expanded () { return this.button.active; }, 93 | set expanded (val) { this.button.active = val; }, 94 | 95 | get label () { return this.info.label.label; }, 96 | set label (val) { 97 | val = val || ""; 98 | this.info.label.set_text (val); //"\u26A1 " + text; 99 | }, 100 | 101 | add_item: function (item) { 102 | this.section.add (item); 103 | item.connect ("clicked", () => {this.expanded = false;}); 104 | }, 105 | 106 | remove_all: function () { 107 | this.section.get_children().forEach (p => {p.destroy ()}); 108 | } 109 | }); 110 | 111 | var SideItem = new Lang.Class({ 112 | Name: "SideItem", 113 | Extends: Gtk.Button, 114 | 115 | _init: function (text, tooltip, info) { 116 | tooltip = tooltip || ""; 117 | info = info || ""; 118 | this.parent ({tooltip_text:tooltip, xalign:0}); 119 | this.info = new InfoLabel ({no_show_all:false}); 120 | this.info.label.set_text (text); 121 | this.info.info.set_text (info); 122 | this.info.info.xalign = 1; 123 | this.info.info.get_style_context ().add_class ("infolabel"); 124 | this.add (this.info); 125 | this.get_style_context ().add_class ("sideitem"); 126 | this.set_relief (Gtk.ReliefStyle.NONE); 127 | this.show_all (); 128 | } 129 | }); 130 | 131 | var InfoLabel = new Lang.Class({ 132 | Name: "InfoLabel", 133 | Extends: Gtk.Box, 134 | 135 | _init: function (props={}) { 136 | props.orientation = props.orientation || Gtk.Orientation.HORIZONTAL; 137 | this.parent (props); 138 | 139 | this.label = new Gtk.Label ({label:"", xalign:0.0, margin_left:8}); 140 | this.add (this.label); 141 | 142 | this.info = new Gtk.Label ({label:"", xalign:0.0, margin_left:8}); 143 | this.pack_start (this.info, true, true, 8); 144 | this.label.connect ("notify::label", this.on_label.bind (this)); 145 | this.info.connect ("notify::label", this.on_label.bind (this)); 146 | }, 147 | 148 | on_label: function (o) { 149 | this.visible = o.visible = !!o.label; 150 | }, 151 | 152 | update: function (info) { 153 | info = info || ""; 154 | if (this.info.label != info) this.info.set_text (info); 155 | } 156 | }); 157 | -------------------------------------------------------------------------------- /common/SubscriptionView.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Gio = imports.gi.Gio; 12 | const Gtk = imports.gi.Gtk; 13 | const GdkPixbuf = imports.gi.GdkPixbuf; 14 | const Lang = imports.lang; 15 | 16 | const Logger = imports.common.Logger; 17 | const Utils = imports.common.Utils; 18 | const ResultView = imports.common.ResultView; 19 | const ItemView = imports.common.ItemView; 20 | 21 | let IPP = 60; //items per page 22 | let APPDIR = ""; 23 | 24 | var SubscriptionView = new Lang.Class({ 25 | Name: "SubscriptionView", 26 | Extends: ResultView.ResultView, 27 | 28 | _init: function (parent) { 29 | this.parent (parent); 30 | this.settings = parent.settings; 31 | APPDIR = parent.application.current_dir; 32 | this.results.max_children_per_line = 5; 33 | this.activate_on_single_click = true, 34 | this.results.homogeneous = true; 35 | }, 36 | 37 | query: function (page) { 38 | page = page || 0; 39 | if (page*IPP >= this.settings.channels.length) return; 40 | let marks = this.settings.channels.slice (page*IPP, (page+1)*IPP); 41 | if (!marks.length) return; 42 | if (page) this.pager.prev.token = (page - 1).toString (); 43 | else this.pager.prev.token = ""; 44 | if ((this.settings.channels.length - (page+1)*IPP) > 0) this.pager.next.token = (page + 1).toString (); 45 | else this.pager.next.token = ""; 46 | this.pager.toggle (); 47 | this.clear_all (); 48 | marks.forEach (p => { 49 | let item = new SubscriptionViewItem (this.settings.get_view_history_item (p)); 50 | //item.show_details (); 51 | this.results.add (item); 52 | }); 53 | if (this.scroll) this.scroll.vadjustment.value = 0; 54 | }, 55 | 56 | on_page_selected: function (o, token) { 57 | this.query (parseInt (token)); 58 | }, 59 | 60 | on_item_activated: function (o, item) { 61 | var child = item.get_children()[0]; 62 | if (child && child.channel) { 63 | let app = Gio.Application.get_default (); 64 | app.window.channelview.load (child.channel, child.pixbuf); 65 | this.w.back.last = this.w.stack.visible_child_name; 66 | } 67 | } 68 | }); 69 | 70 | var SubscriptionViewItem = new Lang.Class({ 71 | Name: "SubscriptionViewItem", 72 | Extends: Gtk.Box, 73 | 74 | _init: function (data) { 75 | this.parent ({orientation:Gtk.Orientation.HORIZONTAL, margin:2, spacing:8}); 76 | this.hexpand = false; 77 | 78 | this.channel = data; 79 | //print (JSON data); 80 | this.tooltip_text = this.channel.title; 81 | 82 | this.image = new Gtk.Image (); 83 | this.image.pixbuf = GdkPixbuf.Pixbuf.new_from_file (APPDIR + "/data/icons/newstream.item.svg").scale_simple (48, 30, 2); 84 | this.add (this.image); 85 | 86 | let box = new Gtk.Box ({orientation:Gtk.Orientation.VERTICAL}); 87 | //box.get_style_context ().add_class ("sb"); 88 | this.pack_start (box, true, true, 8); 89 | 90 | this.title = new Gtk.Label ({ 91 | label:this.channel.title, xalign:0, wrap: true, lines: 1, ellipsize: 3 92 | }); 93 | this.title.max_width_chars = 64; 94 | box.pack_start (this.title, false, false, 0); 95 | 96 | let d = new Date (this.channel.published).toLocaleDateString ("lookup", { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) || ""; 97 | this.published = new Gtk.Label ({label:d, xalign:0, opacity: 0.7}); 98 | this.published.get_style_context ().add_class ("small"); 99 | box.pack_start (this.published, true, true, 0); 100 | 101 | this.bookmark = new ItemView.BookButton (); 102 | this.settings = Gio.Application.get_default ().window.settings; 103 | this.bookmark.set_bookmark (this.settings.subscribed (this.channel.id)); 104 | this.bookmark.editor.setup_channel (this.channel); 105 | this.pack_end (this.bookmark, false, false, 0); 106 | 107 | let app = Gio.Application.get_default (); 108 | if (this.channel.thumbnails) this.get_thumb (); 109 | else if (app.window) app.window.provider.get_channel_info (this.channel.id, (d) => { 110 | if (!d || (d.kind != "youtube#channel")) return; 111 | if (d.snippet.title) this.channel.title = d.snippet.title; 112 | if (d.snippet.publishedAt) this.channel.published = d.snippet.publishedAt; 113 | if (d.snippet.description) this.channel.description = d.snippet.description; 114 | if (d.snippet.thumbnails) this.channel.thumbnails = d.snippet.thumbnails; 115 | if (!d.statistics) return; 116 | if (d.statistics.viewCount) this.channel.views = d.statistics.viewCount; 117 | if (d.statistics.subscriberCount) this.channel.subscribers = d.statistics.subscriberCount; 118 | if (d.statistics.videoCount) this.channel.videos = d.statistics.videoCount; 119 | app.window.settings.add_data (this.channel); 120 | this.get_thumb (); 121 | }); 122 | this.show_all (); 123 | }, 124 | 125 | get_thumb: function () { 126 | let url = this.channel.thumbnails["default"].url; 127 | if (url) Utils.fetch (url, null, null, (d, r) => { 128 | if (r != 200) return; 129 | try { 130 | this.pixbuf = GdkPixbuf.Pixbuf.new_from_stream (Gio.MemoryInputStream.new_from_bytes (d), null); 131 | this.image.pixbuf = this.pixbuf.scale_simple (32, 32, 2); 132 | } catch (e) {debug (e.message);}; 133 | }); 134 | } 135 | }); 136 | 137 | const DOMAIN = "Subscriptions"; 138 | function error (msg) {Logger.error (DOMAIN, msg)} 139 | function debug (msg) {Logger.debug (DOMAIN, msg)} 140 | function info (msg) {Logger.info (DOMAIN, msg)} 141 | -------------------------------------------------------------------------------- /common/Utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a part of NewStream package 3 | * Copyright (C) 2018-2019 konkor 4 | * 5 | * Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * You should have received a copy of the GNU General Public License along 8 | * with this program. If not, see . 9 | */ 10 | 11 | const Lang = imports.lang; 12 | const ByteArray = imports.byteArray; 13 | const GLib = imports.gi.GLib; 14 | const Gio = imports.gi.Gio; 15 | const Soup = imports.gi.Soup; 16 | 17 | const Logger = imports.common.Logger; 18 | 19 | const USER_AGENT = 'GNOME Shell - YouTubeSearchProvider - extension'; 20 | 21 | function fetch (url, agent, headers, callback) { 22 | agent = agent || USER_AGENT; 23 | if (!callback) return; 24 | 25 | let session = new Soup.SessionAsync({ user_agent: agent }); 26 | Soup.Session.prototype.add_feature.call (session, new Soup.ProxyResolverDefault()); 27 | let request = Soup.Message.new ("GET", url); 28 | if (headers) headers.forEach (h=>{ 29 | request.request_headers.append (h[0], h[1]); 30 | }); 31 | session.queue_message (request, (source, message) => { 32 | if (callback) { 33 | //callback (message.response_body.data.toString()?message.response_body.data:"", message.status_code); 34 | if (message.status_code != 200) error ("ERROR: Fetching data: status code: " + message.status_code + "\n" + message.response_body_data.get_data ()); 35 | callback (message.response_body_data.get_data (), message.status_code); 36 | } 37 | }); 38 | } 39 | 40 | function spawn_async (args, callback) { 41 | callback = callback || null; 42 | let r, pid; 43 | try { 44 | [r, pid] = GLib.spawn_async (null, args, null, 45 | GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, null); 46 | } catch (e) { 47 | error (e.message); 48 | return; 49 | } 50 | GLib.child_watch_add (GLib.PRIORITY_DEFAULT, pid, (p, s, o) => { 51 | if (callback) callback (p, s, o); 52 | }); 53 | } 54 | 55 | let ydl = ""; 56 | 57 | function fetch_formats (id, callback) { 58 | if (!callback) return; 59 | let data = {}; 60 | 61 | if (!ydl) ydl = GLib.find_program_in_path ("youtube-dl"); 62 | if (!ydl) return; 63 | let pipe = new SpawnPipe ([ydl, "--all-formats", "--dump-single-json", "https://www.youtube.com/watch?v=" + id], "/", 64 | (info, error) => { 65 | if (error) Logger.error ("FETCH_FORMATS", error); 66 | try { 67 | if (info) data = JSON.parse (info); 68 | } catch (e) { 69 | Logger.error ("FETCH_FORMATS", e); 70 | callback ({}); 71 | } finally { 72 | callback (data); 73 | } 74 | }); 75 | } 76 | 77 | var SpawnPipe = new Lang.Class({ 78 | Name: 'SpawnPipe', 79 | 80 | _init: function (args, dir, callback) { 81 | dir = dir || "/"; 82 | let exit, pid, stdin_fd, stdout_fd, stderr_fd; 83 | this.error = ""; 84 | this.stdout = []; 85 | this.dest = ""; 86 | 87 | try { 88 | [exit, pid, stdin_fd, stdout_fd, stderr_fd] = 89 | GLib.spawn_async_with_pipes (dir,args,null,GLib.SpawnFlags.DO_NOT_REAP_CHILD,null); 90 | GLib.close (stdin_fd); 91 | let outchannel = GLib.IOChannel.unix_new (stdout_fd); 92 | GLib.io_add_watch (outchannel,100,GLib.IOCondition.IN | GLib.IOCondition.HUP, (channel, condition) => { 93 | return this.process_line (channel, condition, "stdout"); 94 | }); 95 | let errchannel = GLib.IOChannel.unix_new (stderr_fd); 96 | GLib.io_add_watch (errchannel,100,GLib.IOCondition.IN | GLib.IOCondition.HUP, (channel, condition) => { 97 | return this.process_line (channel, condition, "stderr"); 98 | }); 99 | let watch = GLib.child_watch_add (100, pid, (pid, status, o) => { 100 | //print ("watch handler " + pid + ":" + status + ":" + o); 101 | GLib.source_remove (watch); 102 | GLib.spawn_close_pid (pid); 103 | if (callback) callback (this.stdout, this.error); 104 | }); 105 | } catch (e) { 106 | error (e); 107 | } 108 | }, 109 | 110 | process_line: function (channel, condition, stream_name) { 111 | if (condition == GLib.IOCondition.HUP) { 112 | //debug (stream_name, ": has been closed"); 113 | return false; 114 | } 115 | try { 116 | var [,line,] = channel.read_line (), i = -1; 117 | if (line) { 118 | //print (stream_name, line); 119 | if (stream_name == "stderr") { 120 | this.error = line; 121 | } else { 122 | this.stdout.push (line); 123 | } 124 | } 125 | } catch (e) { 126 | return false; 127 | } 128 | return true; 129 | } 130 | }); 131 | 132 | function launch_uri (uri) { 133 | let app = Gio.AppInfo.get_default_for_uri_scheme ("https"); 134 | if (!app || !uri) return; 135 | try { 136 | if (app) app.launch_uris ([uri], null); 137 | } catch (e) { 138 | print (e); 139 | } 140 | } 141 | 142 | function age (date) { 143 | let s = "just now"; 144 | if (!date) return ""; 145 | var d = new Date (Date.now () - date); 146 | //print (d); 147 | var y = d.getUTCFullYear() - 1970, m = d.getUTCMonth(),days = d.getUTCDate()-1; 148 | var h = d.getUTCHours(), min = d.getUTCMinutes(), sec = d.getUTCSeconds(); 149 | if (y > 1) s = y + " years ago"; 150 | else if (y > 0) s = "1 year ago"; 151 | else if (m > 1) s = m + " months ago"; 152 | else if (m > 0) s = "1 month ago"; 153 | else if (days > 1) s = days + " days ago"; 154 | else if (days > 0) s = "1 day ago"; 155 | else if (h > 1) s = h + " hours ago"; 156 | else if (h > 0) s = "1 hour ago"; 157 | else if (min > 1) s = min + " minutes ago"; 158 | else if (min > 0) s = "1 minute ago"; 159 | 160 | return s; 161 | } 162 | 163 | function format_size (number) { 164 | let s = "0"; 165 | if (!number) return s; 166 | 167 | if (number >= 1000000000) s = get_round (number, 1000000000) + "B"; 168 | else if (number >= 1000000) s = get_round (number, 1000000) + "M"; 169 | else if (number >= 1000) s = get_round (number, 1000) + "K"; 170 | else s = number.toString(); 171 | 172 | return s; 173 | } 174 | 175 | function format_size_long (number) { 176 | let s = "", n; 177 | if (!number) return "0"; 178 | n = number.toString ().trim (); s = ""; 179 | if (!n) return "0"; 180 | 181 | for (let i = n.length - 1; i > -1; i -= 3) { 182 | var start = i - 2; 183 | if (start < 0) start = 0; 184 | s = n.substring (start, i + 1) + " " + s; 185 | } 186 | 187 | return s.trim (); 188 | } 189 | function get_round (number, base) { 190 | if (!base) return "0"; 191 | var s = Math.round (number/base, 1).toString(); 192 | if (s.length > 3) 193 | s = Math.round (number/base, 0).toString(); 194 | return s; 195 | } 196 | 197 | function time_stamp (time) { 198 | time = time || 0; 199 | let h = 0, m = 0, s = 0; 200 | let t = parseInt (Math.round (time)); 201 | if (!Number.isInteger (t)) t = 0; 202 | s = t % 60; 203 | t = parseInt (t / 60); 204 | m = t % 60; 205 | t = parseInt (t / 60); 206 | h = t % 60; 207 | 208 | if (h) return "%d:%02d:%02d".format (h,m,s); 209 | else return "%d:%02d".format (m,s); 210 | } 211 | 212 | let current_version = ""; 213 | let latest_version = ""; 214 | function check_install_ydl () { 215 | let path = GLib.build_filenamev ([get_user_bin_dir (),"youtube-dl"]); 216 | let file = Gio.File.new_for_path (path); 217 | if (!file.query_exists (null)) 218 | return false; 219 | let info = file.query_info ("*", 0, null); 220 | if (!info.get_attribute_boolean (Gio.FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE)) { 221 | info.set_attribute_boolean (Gio.FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, true); 222 | let cmd = GLib.find_program_in_path ("chmod"); 223 | if (!cmd) return false; 224 | GLib.spawn_command_line_sync (cmd + " a+rx " + path); 225 | if (!info.get_attribute_boolean (Gio.FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE)) 226 | return false; 227 | } 228 | ydl = path; 229 | latest_version = current_version = get_info_string (ydl + " --version"); 230 | 231 | return true; 232 | } 233 | 234 | function install_ydl (callback) { 235 | fetch ("https://yt-dl.org/downloads/latest/youtube-dl", 236 | "New Stream (GNU/Linux)", null, (data, s) => { 237 | if ((s == 200) && data) { 238 | let file = Gio.File.new_for_path (get_user_bin_dir () + "/youtube-dl"); 239 | file.replace_contents_bytes_async ( 240 | data, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null, (o, res) => { 241 | file.replace_contents_finish (res); 242 | check_install_ydl (); 243 | if (callback) callback (); 244 | } 245 | ); 246 | } 247 | return false; 248 | }); 249 | return true; 250 | } 251 | 252 | function check_update_ydl (callback) { 253 | fetch ("https://rg3.github.io/youtube-dl/update/LATEST_VERSION", 254 | null, null, (text, s) => { 255 | if ((s == 200) && text) { 256 | text = bytesToString (text).toString (); 257 | if ((text.length > 6) && (text.length < 20) && (text.split("\n").length < 2)) 258 | latest_version = text.split("\n")[0]; 259 | } 260 | if (latest_version != current_version) { 261 | install_ydl (); 262 | if (callback) callback (); 263 | } 264 | return false; 265 | }); 266 | } 267 | 268 | function bytesToString (array) { 269 | return array instanceof Uint8Array ? ByteArray.toString (array) : array; 270 | } 271 | 272 | function get_user_bin_dir () { 273 | let path = GLib.build_filenamev ([GLib.get_home_dir (), ".local/bin"]); 274 | if (!GLib.file_test (path, GLib.FileTest.EXISTS)) 275 | GLib.mkdir_with_parents (path, 484); 276 | return path; 277 | } 278 | 279 | function get_app_data_dir () { 280 | let path = GLib.build_filenamev ([GLib.get_user_data_dir(),"newstream"]); 281 | if (!GLib.file_test (path, GLib.FileTest.EXISTS)) 282 | GLib.mkdir_with_parents (path, 484); 283 | return path; 284 | } 285 | 286 | let cmd_out, info_out; 287 | function get_info_string (cmd) { 288 | cmd_out = GLib.spawn_command_line_sync (cmd); 289 | if (cmd_out[0]) info_out = bytesToString (cmd_out[1]).toString().split("\n")[0]; 290 | if (info_out) return info_out; 291 | return ""; 292 | } 293 | 294 | const DOMAIN = "Utils"; 295 | function error (msg) {Logger.error (DOMAIN, msg)} 296 | function debug (msg) {Logger.debug (DOMAIN, msg)} 297 | function info (msg) {Logger.info (DOMAIN, msg)} 298 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | dnl Process this file with autoconf to produce a configure script. 2 | 3 | AC_INIT(newstream, [0.1.6]) 4 | 5 | AC_CONFIG_HEADERS([config.h]) 6 | 7 | AC_CONFIG_AUX_DIR([.]) 8 | 9 | AM_INIT_AUTOMAKE([1.11 dist-xz no-dist-gzip foreign tar-ustar]) 10 | 11 | AM_SILENT_RULES([yes]) 12 | 13 | LT_INIT 14 | 15 | GLIB_GSETTINGS 16 | 17 | dnl Packaging version 18 | echo "${VERSION}" > packaging/VERSION 19 | 20 | AC_OUTPUT([ 21 | Makefile 22 | common/Makefile 23 | data/Makefile 24 | data/icons/Makefile 25 | data/themes/Makefile 26 | schemas/Makefile 27 | ]) 28 | 29 | echo " 30 | 31 | ${PACKAGE} ${VERSION} 32 | ========= 33 | " 34 | -------------------------------------------------------------------------------- /data/Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = icons themes 2 | 3 | # The desktop files 4 | desktopdir = $(datadir)/applications 5 | desktop_DATA = \ 6 | io.github.konkor.newstream.desktop \ 7 | $(NULL) 8 | 9 | EXTRA_DIST = \ 10 | $(desktop_DATA) 11 | -------------------------------------------------------------------------------- /data/icons/Makefile.am: -------------------------------------------------------------------------------- 1 | #application data icons 2 | iconsdir=$(datadir)/newstream/data/icons 3 | icons_DATA= \ 4 | back-symbolic.svg \ 5 | folder-saved-search-symbolic.svg \ 6 | headphones-symbolic.svg \ 7 | history-symbolic.svg \ 8 | io.github.konkor.newstream.svg \ 9 | newstream.item.svg \ 10 | newstream.cover.svg \ 11 | window-close-symbolic.svg\ 12 | open-menu-symbolic.svg\ 13 | edit-copy-symbolic.svg\ 14 | author.svg\ 15 | bookmark_off.svg\ 16 | share.svg 17 | 18 | #application social icons 19 | socialdir=$(datadir)/newstream/data/icons/social 20 | social_DATA= \ 21 | social/gplus.png \ 22 | social/fb.png \ 23 | social/twit.png \ 24 | social/red.png \ 25 | social/id.png \ 26 | social/mail.svg \ 27 | social/link.svg 28 | 29 | #system application icons 30 | icondir=$(datadir)/icons 31 | nobase_icon_DATA = hicolor/scalable/apps/io.github.konkor.newstream.svg \ 32 | hicolor/32x32/apps/io.github.konkor.newstream.png \ 33 | $(NULL) 34 | 35 | #the application icon 36 | pixmapsdir=$(datadir)/pixmaps 37 | pixmaps_DATA= \ 38 | hicolor/scalable/apps/io.github.konkor.newstream.svg 39 | 40 | EXTRA_DIST = \ 41 | $(icons_DATA) \ 42 | $(social_DATA) \ 43 | $(pixmaps_DATA) \ 44 | $(nobase_icon_DATA) 45 | 46 | gtk_update_icon_cache = gtk-update-icon-cache -f -t $(datadir)/icons/hicolor 47 | 48 | install-data-hook: update-icon-cache 49 | 50 | uninstall-hook: update-icon-cache 51 | 52 | update-icon-cache: 53 | @-if test -z "$(DESTDIR)"; then \ 54 | echo "Updating Gtk icon cache."; \ 55 | $(gtk_update_icon_cache); \ 56 | else \ 57 | echo "*** Icon cache not updated. After (un)install, run this:"; \ 58 | echo "*** $(gtk_update_icon_cache)"; \ 59 | fi 60 | -------------------------------------------------------------------------------- /data/icons/author.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /data/icons/back-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 59 | 65 | 66 | -------------------------------------------------------------------------------- /data/icons/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/bookmark_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /data/icons/edit-copy-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | image/svg+xml 9 | 10 | Gnome Symbolic Icon Theme 11 | 12 | 13 | 14 | 15 | 16 | 17 | Gnome Symbolic Icon Theme 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /data/icons/folder-saved-search-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | image/svg+xml 9 | 10 | Gnome Symbolic Icon Theme 11 | 12 | 13 | 14 | 15 | 16 | 17 | Gnome Symbolic Icon Theme 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /data/icons/headphones-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 54 | 60 | 66 | 71 | 77 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /data/icons/hicolor/32x32/apps/io.github.konkor.newstream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/data/icons/hicolor/32x32/apps/io.github.konkor.newstream.png -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/io.github.konkor.newstream.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 33 | 35 | 38 | 42 | 46 | 47 | 56 | 57 | 77 | 82 | 90 | 91 | -------------------------------------------------------------------------------- /data/icons/history-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 59 | 64 | 69 | 74 | 75 | -------------------------------------------------------------------------------- /data/icons/io.github.konkor.newstream.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 56 | 64 | 65 | -------------------------------------------------------------------------------- /data/icons/newstream.cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 59 | 66 | 70 | 83 | 96 | 109 | 110 | 116 | 122 | 123 | -------------------------------------------------------------------------------- /data/icons/newstream.item.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 56 | 64 | 65 | -------------------------------------------------------------------------------- /data/icons/open-menu-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 25 | 27 | image/svg+xml 28 | 30 | Gnome Symbolic Icon Theme 31 | 32 | 33 | 34 | 70 | 82 | 83 | Gnome Symbolic Icon Theme 85 | 87 | 93 | 99 | 104 | 110 | 115 | 121 | 127 | 133 | 139 | 147 | 155 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /data/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 54 | 59 | 64 | 69 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/icons/social/fb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/data/icons/social/fb.png -------------------------------------------------------------------------------- /data/icons/social/fb.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 54 | 59 | 64 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /data/icons/social/gplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/data/icons/social/gplus.png -------------------------------------------------------------------------------- /data/icons/social/gplus.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 56 | 63 | 64 | -------------------------------------------------------------------------------- /data/icons/social/id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/data/icons/social/id.png -------------------------------------------------------------------------------- /data/icons/social/id.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 54 | 59 | 65 | 66 | -------------------------------------------------------------------------------- /data/icons/social/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 58 | 59 | -------------------------------------------------------------------------------- /data/icons/social/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 59 | 60 | -------------------------------------------------------------------------------- /data/icons/social/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/data/icons/social/red.png -------------------------------------------------------------------------------- /data/icons/social/red.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 54 | 59 | 65 | 66 | -------------------------------------------------------------------------------- /data/icons/social/twit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/data/icons/social/twit.png -------------------------------------------------------------------------------- /data/icons/social/twit.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 56 | 62 | 63 | -------------------------------------------------------------------------------- /data/icons/window-close-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gnome Symbolic Icon Theme 6 | 7 | 8 | 9 | image/svg+xml 10 | 11 | Gnome Symbolic Icon Theme 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/io.github.konkor.newstream.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=New Stream 3 | Comment=Youtube Stream Player 4 | Exec=new-stream 5 | Icon=io.github.konkor.newstream 6 | Terminal=false 7 | Type=Application 8 | Categories=GTK;GNOME;AudioVideo;Player;Video; 9 | Keywords=newstream;Youtube;Stream;Player;Video;Movie;Film;Clip;Series;Internet; 10 | StartupNotify=true 11 | Name[en_US]=New Stream 12 | -------------------------------------------------------------------------------- /data/themes/Makefile.am: -------------------------------------------------------------------------------- 1 | #themes 2 | defsdir=$(datadir)/newstream/data/themes/default 3 | defs_DATA= \ 4 | default/gtk.css 5 | 6 | EXTRA_DIST = \ 7 | $(defs_DATA) 8 | 9 | -------------------------------------------------------------------------------- /data/themes/default/gtk.css: -------------------------------------------------------------------------------- 1 | @define-color newstream_base_color shade (@theme_base_color, 0.9); 2 | @define-color newstream_fg_color shade (@theme_fg_color, 0.9); 3 | @define-color newstream_bg_color shade (@theme_bg_color, 0.9); 4 | @define-color newstream_selection_color @theme_selected_bg_color; 5 | 6 | * { 7 | font-family: Roboto, Cantarell, Sans-Serif; 8 | } 9 | 10 | .small { 11 | font-size: 85%; 12 | } 13 | 14 | .channel-button { 15 | border-radius: 0; 16 | border-style:none; 17 | box-shadow:none; 18 | outline-width:0; 19 | border-image:none; 20 | background-image: none; 21 | } 22 | .channel-button:active { 23 | background-color: @newstream_bg_color; 24 | } 25 | .bookmark { 26 | margin: 0; 27 | }.selected { 28 | color: @newstream_selection_color; 29 | } 30 | -------------------------------------------------------------------------------- /data/themes/tube/gtk.css: -------------------------------------------------------------------------------- 1 | @define-color newstream_base_color shade (@theme_base_color, 0.9); 2 | @define-color newstream_fg_color shade (@theme_fg_color, 0.9); 3 | @define-color newstream_bg_color shade (@theme_bg_color, 0.9); 4 | 5 | * { 6 | font-family: Roboto, Cantarell, Sans-Serif; 7 | } 8 | 9 | .small { 10 | font-size: 85%; 11 | } 12 | 13 | .hb { 14 | border:0; 15 | border-style:none; 16 | border-image:none; 17 | box-shadow:none; 18 | outline-width:0px; 19 | padding:0; 20 | text-shadow:none; 21 | color: #cfcfcf; 22 | background-color: #a10705; 23 | background-image: none; 24 | font-weight: bold; 25 | } 26 | .titlebar { 27 | padding:0 1em 0 0.2em; 28 | } 29 | .sb { 30 | color: #bfbfbf; 31 | background-color: @newstream_bg_color; 32 | font-size: 1.2em; 33 | } 34 | .sb-button { 35 | border-radius: 0; 36 | border-style:none; 37 | box-shadow:none; 38 | outline-width:0; 39 | border-image:none; 40 | font-size: 1em; 41 | padding:1em; 42 | margin:0; 43 | border:0; 44 | color: @newstream_fg_color; 45 | text-shadow:none; 46 | background-image: none; 47 | border-right: 1px solid alpha(#fff, 0.47); 48 | } 49 | .sb-button:last-child { 50 | border:0; 51 | } 52 | .sb-button:checked, .sb-button:hover, .sb-preferences:checked, .sb-preferences:hover { 53 | color: @newstream_fg_color; 54 | background-color: @newstream_base_color; 55 | } 56 | .sb-button:checked { 57 | font-weight: bold; 58 | } 59 | 60 | .hb-button, .hb GtkButton, .hb button, .hb GtkMenuButton { 61 | margin:0; 62 | padding:6px 8px; 63 | outline-width:0; 64 | border: 0; 65 | border-style:none; 66 | box-shadow:none; 67 | border-image:none; 68 | background-image: none; 69 | color: #cfcfcf; 70 | } 71 | .hb GtkButton:hover, .hb button:hover { 72 | color: #fff; 73 | /*background-color: #7a0000;*/ 74 | } 75 | 76 | .search-bar { 77 | color: #cfcfcf; 78 | background-color: #a10705; 79 | border:0; 80 | box-shadow:none; 81 | padding:6px 8px; 82 | } 83 | .search-entry { 84 | color: #cfcfcf; 85 | background-image: none; 86 | box-shadow:none; 87 | border-image:none; 88 | border:0; 89 | padding:6px 8px; 90 | } 91 | .search-entry:focus, .search-entry:hover{ 92 | border:1px solid; 93 | } 94 | -------------------------------------------------------------------------------- /new-stream: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gjs 2 | /* 3 | * NewStream - Youtube player 4 | * 5 | * Copyright (C) 2018-2020 konkor 6 | * 7 | * This file is part of NewStream. 8 | * 9 | * NewStream is free software: you can redistribute it and/or modify it 10 | * under the terms of the GNU General Public License as published by the 11 | * Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * NewStream is distributed in the hope that it will be useful, but 15 | * WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 17 | * See the GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License along 20 | * with this program. If not, see . 21 | */ 22 | 23 | const Gio = imports.gi.Gio; 24 | const GLib = imports.gi.GLib; 25 | const System = imports.system; 26 | 27 | const APPDIR = get_appdir (); 28 | imports.searchPath.unshift(APPDIR); 29 | const Application = imports.common.Application; 30 | 31 | let app = new Application.NewStreamApplication ({}); 32 | ARGV.unshift (System.programInvocationName); 33 | app.run (ARGV); 34 | 35 | function getCurrentFile () { 36 | let stack = (new Error()).stack; 37 | let stackLine = stack.split('\n')[1]; 38 | if (!stackLine) 39 | throw new Error ('Could not find current file'); 40 | let match = new RegExp ('@(.+):\\d+').exec(stackLine); 41 | if (!match) 42 | throw new Error ('Could not find current file'); 43 | let path = match[1]; 44 | let file = Gio.File.new_for_path (path); 45 | return [file.get_path(), file.get_parent().get_path(), file.get_basename()]; 46 | } 47 | 48 | function get_appdir () { 49 | let s = getCurrentFile ()[1]; 50 | if (GLib.file_test (s + "/common/Application.js", GLib.FileTest.EXISTS)) return s; 51 | s = GLib.get_home_dir () + "/.local/share/newstream"; 52 | if (GLib.file_test (s + "/common/Application.js", GLib.FileTest.EXISTS)) return s; 53 | s = "/usr/local/share/newstream"; 54 | if (GLib.file_test (s + "/common/Application.js", GLib.FileTest.EXISTS)) return s; 55 | s = "/usr/share/newstream"; 56 | if (GLib.file_test (s + "/common/Application.js", GLib.FileTest.EXISTS)) return s; 57 | throw "NewStream installation not found..."; 58 | return s; 59 | } 60 | -------------------------------------------------------------------------------- /packaging/debian/changelog: -------------------------------------------------------------------------------- 1 | newstream (0.1.6-1) UNRELEASED; urgency=low 2 | 3 | * Update fixes and improvements 4 | 5 | -- konkor Sat, 5 Oct 2019 16:00:00 +0200 6 | 7 | newstream (0.1.5-1) UNRELEASED; urgency=low 8 | 9 | * Add Volume Button 10 | * Add Fullscreen Button 11 | * Update refactoring, minor fixes and improvements 12 | 13 | -- konkor Sat, 13 Apr 2019 11:31:00 +0200 14 | 15 | newstream (0.1.4-1) UNRELEASED; urgency=low 16 | 17 | * Add subscription view 18 | * Add channel view 19 | * Update minor fixes and improvements 20 | 21 | -- konkor Wed, 10 Apr 2019 01:24:00 +0200 22 | 23 | newstream (0.1.3-1) UNRELEASED; urgency=low 24 | 25 | * Add local history of views 26 | * Add local bookmarks 27 | * Update minor fixes and improvements 28 | 29 | -- konkor Sat, 17 Nov 2018 18:06:00 +0200 30 | 31 | newstream (0.1.2-1) UNRELEASED; urgency=low 32 | 33 | * Add video description 34 | * Add item toolbar 35 | * Add video covers 36 | * Update minor fixes and improvements 37 | 38 | -- konkor Tue, 13 Nov 2018 18:10:00 +0200 39 | 40 | newstream (0.1.1-1) UNRELEASED; urgency=low 41 | 42 | * Initial release 43 | 44 | -- konkor Thu, 08 Nov 2018 23:52:00 +0200 45 | -------------------------------------------------------------------------------- /packaging/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /packaging/debian/control: -------------------------------------------------------------------------------- 1 | Source: newstream 2 | Maintainer: konkor 3 | Section: misc 4 | Priority: optional 5 | Standards-Version: 3.9.2 6 | Build-Depends: debhelper (>= 9), 7 | dh-autoreconf, 8 | intltool 9 | 10 | Package: newstream 11 | Architecture: all 12 | Depends: ${shlibs:Depends}, 13 | ${misc:Depends}, 14 | gir1.2-gtk-3.0, 15 | gir1.2-gtkclutter-1.0, 16 | gir1.2-clutter-gst-3.0 | gir1.2-clutter-gst-1.0, 17 | gir1.2-gdkpixbuf-2.0, 18 | gir1.2-soup-2.4, 19 | gjs, 20 | gstreamer1.0-plugins-base-apps, 21 | gstreamer1.0-nice, 22 | gstreamer1.0-libav, 23 | gstreamer1.0-plugins-base, 24 | gstreamer1.0-plugins-good, 25 | gstreamer1.0-plugins-bad, 26 | gstreamer1.0-plugins-ugly 27 | Recommends: fonts-roboto, xclip 28 | Description: Youtube Player 29 | Linux Youtube Stream Player. 30 | -------------------------------------------------------------------------------- /packaging/debian/copyright: -------------------------------------------------------------------------------- 1 | Upstream Author: 2 | Copyright (C) 2018 konkor 3 | 4 | Copyright: 5 | 6 | Copyright © 2018 Copyright (C) 2018 konkor 7 | 8 | License: 9 | 10 | * NewStream is free software: you can redistribute it and/or modify it 11 | * under the terms of the GNU General Public License as published by the 12 | * Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * NewStream is distributed in the hope that it will be useful, but 16 | * WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 18 | * See the GNU General Public License for more details. 19 | 20 | On Debian GNU/Linux systems, the complete text of the GNU General Public 21 | License can be found in `/usr/share/common-licenses/GPL'. 22 | -------------------------------------------------------------------------------- /packaging/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | %: 3 | dh $@ 4 | -------------------------------------------------------------------------------- /packaging/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /packaging/packaging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PKG_NAME="newstream" 4 | VERSION=$(cat VERSION) 5 | 6 | echo Version: $VERSION 7 | rm -rf debs 8 | mkdir debs 9 | cd debs 10 | ln -s ../../$PKG_NAME-$VERSION.tar.xz newstream_$VERSION.orig.tar.xz 11 | tar xf newstream_$VERSION.orig.tar.xz 12 | cd $PKG_NAME-$VERSION 13 | cp -r ../../debian/ . 14 | debuild -us -uc 15 | -------------------------------------------------------------------------------- /schemas/Makefile.am: -------------------------------------------------------------------------------- 1 | schemadir=$(datadir)/newstream/schemas 2 | schema_DATA= \ 3 | io.github.konkor.newstream.gschema.xml \ 4 | gschemas.compiled 5 | 6 | gsettings_SCHEMAS = io.github.konkor.newstream.gschema.xml 7 | @GSETTINGS_RULES@ 8 | 9 | EXTRA_DIST = \ 10 | $(schema_DATA) \ 11 | $(gsettings_SCHEMAS) \ 12 | $(NULL) 13 | -------------------------------------------------------------------------------- /schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konkor/newstream/79c85c5ae6289ba5315e3e2a0e41edb058617de4/schemas/gschemas.compiled -------------------------------------------------------------------------------- /schemas/io.github.konkor.newstream.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | true 8 | Save last settings 9 | Remember settings for restoring 10 | 11 | 12 | 13 | 5000 14 | History Size 15 | History Size To Remember 16 | 17 | 18 | 19 | 1000 20 | View History Size 21 | Viewed History Size 22 | 23 | 24 | 25 | 480 26 | Window Height 27 | Window Height To Save 28 | 29 | 30 | 31 | 800 32 | Window Width 33 | Window Width To Save 34 | 35 | 36 | 37 | 0 38 | Window x 39 | Window x coordinate 40 | 41 | 42 | 43 | 0 44 | Window y 45 | Window y coordinate 46 | 47 | 48 | 49 | false 50 | Window maximized 51 | Window maximized 52 | 53 | 54 | 55 | 56 | --------------------------------------------------------------------------------