[rhythmbox] add soundcloud plugin



commit a3924db0840cc1035d37db3547ef7cfa91720bcc
Author: Jonathan Matthew <jonathan d14n org>
Date:   Sat Nov 8 21:20:32 2014 +1000

    add soundcloud plugin

 configure.ac                                 |    1 +
 plugins/Makefile.am                          |    1 +
 plugins/soundcloud/Makefile.am               |   16 +
 plugins/soundcloud/powered-by-soundcloud.png |  Bin 0 -> 4519 bytes
 plugins/soundcloud/soundcloud-logo.png       |  Bin 0 -> 406 bytes
 plugins/soundcloud/soundcloud.plugin.in      |   10 +
 plugins/soundcloud/soundcloud.py             |  502 ++++++++++++++++++++++++++
 plugins/soundcloud/soundcloud.ui             |  106 ++++++
 po/POTFILES.in                               |    3 +
 9 files changed, 639 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 449a04c..4dafa8a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -802,6 +802,7 @@ plugins/rbzeitgeist/Makefile
 plugins/notification/Makefile
 plugins/visualizer/Makefile
 plugins/grilo/Makefile
+plugins/soundcloud/Makefile
 sample-plugins/Makefile
 sample-plugins/sample/Makefile
 sample-plugins/sample-python/Makefile
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 3a9ea03..94de29c 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -21,6 +21,7 @@ SUBDIRS +=                                            \
        rbzeitgeist                                     \
        replaygain                                      \
        sendto                                          \
+       soundcloud                                      \
        rb
 
 if WITH_WEBKIT
diff --git a/plugins/soundcloud/Makefile.am b/plugins/soundcloud/Makefile.am
new file mode 100644
index 0000000..8de1b04
--- /dev/null
+++ b/plugins/soundcloud/Makefile.am
@@ -0,0 +1,16 @@
+plugindir = $(PLUGINDIR)/soundcloud
+plugindatadir = $(PLUGINDATADIR)/soundcloud
+plugin_PYTHON = soundcloud.py
+
+plugin_in_files = soundcloud.plugin.in
+%.plugin: %.plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) 
$(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.plugin.in=.plugin)
+
+gtkbuilderdir = $(plugindatadir)
+gtkbuilder_DATA = soundcloud.ui powered-by-soundcloud.png soundcloud-logo.png
+
+EXTRA_DIST = $(plugin_in_files) $(gtkbuilder_DATA)
+
+CLEANFILES = $(plugin_DATA)
+DISTCLEANFILES = $(plugin_DATA)
diff --git a/plugins/soundcloud/powered-by-soundcloud.png b/plugins/soundcloud/powered-by-soundcloud.png
new file mode 100644
index 0000000..a0f5b61
Binary files /dev/null and b/plugins/soundcloud/powered-by-soundcloud.png differ
diff --git a/plugins/soundcloud/soundcloud-logo.png b/plugins/soundcloud/soundcloud-logo.png
new file mode 100644
index 0000000..b43aaf3
Binary files /dev/null and b/plugins/soundcloud/soundcloud-logo.png differ
diff --git a/plugins/soundcloud/soundcloud.plugin.in b/plugins/soundcloud/soundcloud.plugin.in
new file mode 100644
index 0000000..d565817
--- /dev/null
+++ b/plugins/soundcloud/soundcloud.plugin.in
@@ -0,0 +1,10 @@
+[Plugin]
+Loader=python3
+Module=soundcloud
+IAge=2
+_Name=SoundCloud
+_Description=Browse and play sounds from SoundCloud®
+Authors=Jonathan Matthew
+Copyright=Copyright © 2014 Jonathan Matthew
+Website=http://www.rhythmbox.org/
+Depends=rb
diff --git a/plugins/soundcloud/soundcloud.py b/plugins/soundcloud/soundcloud.py
new file mode 100644
index 0000000..a68a2c6
--- /dev/null
+++ b/plugins/soundcloud/soundcloud.py
@@ -0,0 +1,502 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# Copyright (C) 2014 Jonathan Matthew
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+
+from gi.repository import Gtk, Gdk, GObject, Gio, GLib, Peas
+from gi.repository import RB
+import rb
+import urllib.parse
+import json
+from datetime import datetime
+
+import gettext
+gettext.install('rhythmbox', RB.locale_dir())
+
+# rhythmbox app registered with soundcloud with the account notverysmart gmail com
+CLIENT_ID = 'e4ef6572c2baf401db2f64b4e0eae9ce'
+
+class SoundCloudEntryType(RB.RhythmDBEntryType):
+       def __init__(self):
+               RB.RhythmDBEntryType.__init__(self, name='soundcloud')
+
+       def do_get_playback_uri(self, entry):
+               uri = entry.get_string(RB.RhythmDBPropType.MOUNTPOINT)
+               return uri + "?client_id=" + CLIENT_ID
+
+       def do_can_sync_metadata(self, entry):
+               return False
+
+
+class SoundCloudPlugin(GObject.Object, Peas.Activatable):
+       __gtype_name = 'SoundCloudPlugin'
+       object = GObject.property(type=GObject.GObject)
+
+       def __init__(self):
+               GObject.Object.__init__(self)
+
+       def do_activate(self):
+               shell = self.object
+
+               db = shell.props.db
+
+               self.entry_type = SoundCloudEntryType()
+               db.register_entry_type(self.entry_type)
+
+               logofile = rb.find_plugin_file(self, "soundcloud-logo.png")
+               icon = Gio.FileIcon.new(Gio.File.new_for_path(logofile))
+
+               model = RB.RhythmDBQueryModel.new_empty(db)
+               self.source = GObject.new (SoundCloudSource,
+                                          shell=shell,
+                                          name=_("SoundCloud"),
+                                          plugin=self,
+                                          query_model=model,
+                                          entry_type=self.entry_type,
+                                          icon=icon)
+               self.source.setup()
+               group = RB.DisplayPageGroup.get_by_id ("shared")
+               shell.append_display_page(self.source, group)
+
+       def do_deactivate(self):
+               self.source.delete_thyself()
+               self.source = None
+
+class SoundCloudSource(RB.StreamingSource):
+       def __init__(self, **kwargs):
+               super(SoundCloudSource, self).__init__(kwargs)
+               self.loader = None
+
+               self.search_count = 1
+               self.search_types = {
+                       'tracks': {
+                               'label': _("Search tracks"),
+                               'placeholder': _("Search tracks on SoundCloud"),
+                               'title': "",    # container view is hidden
+                               'endpoint': '/tracks.json',
+                               'containers': False
+                       },
+                       'sets': {
+                               'label': _("Search sets"),
+                               'placeholder': _("Search sets on SoundCloud"),
+                               'title': _("SoundCloud Sets"),
+                               'endpoint': '/playlists.json',
+                               'containers': True
+                       },
+                       'users': {
+                               'label': _("Search users"),
+                               'placeholder': _("Search users on SoundCloud"),
+                               'title': _("SoundCloud Users"),
+                               'endpoint': '/users.json',
+                               'containers': True
+                       },
+                       'groups': {
+                               'label': _("Search groups"),
+                               'placeholder': _("Search groups on SoundCloud"),
+                               'title': _("SoundCloud Groups"),
+                               'endpoint': '/groups.json',
+                               'containers': True
+                       },
+               }
+
+               self.container_types = {
+                       'user': {
+                               'attributes': ['username', 'kind', 'uri', 'permalink_url', 'avatar_url', 
'description'],
+                               'tracks-url': '/tracks.json',
+                               'tracks-type': 'plain',
+                       },
+                       'playlist': {
+                               'attributes': ['title', 'kind', 'uri', 'permalink_url', 'artwork_url', 
'description'],
+                               'tracks-url': '.json',
+                               'tracks-type': 'playlist',
+                       },
+                       'group': {
+                               'attributes': ['name', 'kind', 'uri', 'permalink_url', 'artwork_url', 
'description'],
+                               'tracks-url': '/tracks.json',
+                               'tracks-type': 'plain',
+                       }
+               }
+
+       def hide_entry_cb(self, entry):
+               shell = self.props.shell
+               shell.props.db.entry_set(entry, RB.RhythmDBPropType.HIDDEN, True)
+
+       def new_model(self):
+               shell = self.props.shell
+               plugin = self.props.plugin
+               db = shell.props.db
+
+               self.search_count = self.search_count + 1
+               q = GLib.PtrArray()
+               db.query_append_params(q, RB.RhythmDBQueryType.EQUALS, RB.RhythmDBPropType.TYPE, 
plugin.entry_type)
+               db.query_append_params(q, RB.RhythmDBQueryType.EQUALS, RB.RhythmDBPropType.LAST_SEEN, 
self.search_count)
+               model = RB.RhythmDBQueryModel.new_empty(db)
+
+               db.do_full_query_async_parsed(model, q)
+               self.props.query_model = model
+               self.songs.set_model(model)
+
+       def add_track(self, db, entry_type, item):
+               if not item['streamable']:
+                       return
+
+               uri = item['permalink_url']
+               entry = db.entry_lookup_by_location(uri)
+               if entry:
+                       db.entry_set(entry, RB.RhythmDBPropType.LAST_SEEN, self.search_count)
+               else:
+                       entry = RB.RhythmDBEntry.new(db, entry_type, item['permalink_url'])
+                       db.entry_set(entry, RB.RhythmDBPropType.MOUNTPOINT, item['stream_url'])
+                       db.entry_set(entry, RB.RhythmDBPropType.ARTIST, item['user']['username'])
+                       db.entry_set(entry, RB.RhythmDBPropType.TITLE, item['title'])
+                       db.entry_set(entry, RB.RhythmDBPropType.LAST_SEEN, self.search_count)
+                       if item['genre'] is not None:
+                               db.entry_set(entry, RB.RhythmDBPropType.GENRE, item['genre'])
+                       db.entry_set(entry, RB.RhythmDBPropType.DURATION, item['duration']/1000)
+                       if item['bpm'] is not None:
+                               db.entry_set(entry, RB.RhythmDBPropType.BEATS_PER_MINUTE, item['bpm'])
+
+                       if item['description'] is not None:
+                               db.entry_set(entry, RB.RhythmDBPropType.COMMENT, item['description'])
+
+                       if item['artwork_url'] is not None:
+                               db.entry_set(entry, RB.RhythmDBPropType.MB_ALBUMID, item['artwork_url'])
+
+                       if item['release_year'] is not None:
+                               date = GLib.Date.new_dmy(item.get('release_day', 0), 
item.get('release_month', 0), item['release_year'])
+                               db.entry_set(entry, RB.RhythmDBPropType.DATE, date.get_julian())
+
+                       if item['created_at'] is not None:
+                               try:
+                                       dt = datetime.strptime(item['created_at'], '%Y/%m/%d %H:%M:%S %z')
+                                       db.entry_set(entry, RB.RhythmDBPropType.FIRST_SEEN, 
int(dt.timestamp()))
+                               except Exception as e:
+                                       print(str(e))
+
+               db.commit()
+
+       def add_container(self, item):
+               k = item['kind']
+               if k not in self.container_types:
+                       return
+
+               ct = self.container_types[k]
+               self.containers.append([item.get(i) for i in ct['attributes']])
+
+       def search_tracks_api_cb(self, data):
+               if data is None:
+                       return
+
+               shell = self.props.shell
+               db = shell.props.db
+               entry_type = self.props.entry_type
+
+               data = data.decode('utf-8')
+               stuff = json.loads(data)
+               for item in stuff:
+                       self.add_track(db, entry_type, item)
+
+       def search_containers_api_cb(self, data):
+               if data is None:
+                       return
+
+               entry_type = self.props.entry_type
+
+               data = data.decode('utf-8')
+               stuff = json.loads(data)
+               for item in stuff:
+                       self.add_container(item)
+
+       def resolve_api_cb(self, data):
+               if data is None:
+                       return
+
+               data = data.decode('utf-8')
+               stuff = json.loads(data)
+
+               if stuff['kind'] == 'track':
+                       shell = self.props.shell
+                       db = shell.props.db
+                       self.add_track(db, self.props.entry_type, stuff)
+               else:
+                       self.add_container(stuff)
+                       # select, etc. too?
+
+       def playlist_api_cb(self, data):
+               if data is None:
+                       return
+
+               shell = self.props.shell
+               db = shell.props.db
+
+               data = data.decode('utf-8')
+               stuff = json.loads(data)
+               for t in stuff['tracks']:
+                       self.add_track(db, self.props.entry_type, t)
+
+       def cancel_request(self):
+               if self.loader:
+                       self.loader.cancel()
+                       self.loader = None
+
+       def search_popup_cb(self, widget):
+               self.search_popup.popup(None, None, None, None, 3, Gtk.get_current_event_time())
+
+       def search_type_action_cb(self, action, parameter):
+               print(parameter.get_string() + " selected")
+               self.search_type = parameter.get_string()
+
+               if self.search_entry.searching():
+                       self.do_search()
+
+               st = self.search_types[self.search_type]
+               self.search_entry.set_placeholder(st['placeholder'])
+
+       def search_entry_cb(self, widget, term):
+               self.search_text = term
+               self.do_search()
+
+       def do_search(self):
+               self.cancel_request()
+
+               base = 'https://api.soundcloud.com'
+               self.new_model()
+               self.containers.clear()
+               term = self.search_text
+
+               if term.startswith('https://soundcloud.com/') or term.startswith("http://soundcloud.com/";):
+                       # ignore the selected search type and try to resolve whatever the url is
+                       print("resolving " + term)
+                       self.scrolled.hide()
+                       url = base + '/resolve.json?url=' + term + '&client_id=' + CLIENT_ID
+                       self.loader = rb.Loader()
+                       self.loader.get_url(url, self.resolve_api_cb)
+                       return
+
+               if self.search_type not in self.search_types:
+                       print("not sure how to search for " + self.search_type)
+                       return
+
+               print("searching for " + self.search_type + " matching " + term)
+               st = self.search_types[self.search_type]
+               self.container_view.get_column(0).set_title(st['title'])
+
+               url = base + st['endpoint'] + '?q=' + urllib.parse.quote(term) + '&client_id=' + CLIENT_ID
+               self.loader = rb.Loader()
+               if st['containers']:
+                       self.scrolled.show()
+                       self.loader.get_url(url, self.search_containers_api_cb)
+               else:
+                       self.scrolled.hide()
+                       self.loader.get_url(url, self.search_tracks_api_cb)
+
+
+       def selection_changed_cb(self, selection):
+               self.new_model()
+               self.cancel_request()
+               self.build_sc_menu()
+
+               (model, aiter) = selection.get_selected()
+               if aiter is None:
+                       return
+
+               [itemtype, url] = model.get(aiter, 1, 2)
+               if itemtype not in self.container_types:
+                       return
+
+               print("loading %s %s" % (itemtype, url))
+               ct = self.container_types[itemtype]
+               trackurl = url + ct['tracks-url'] + '?client_id=' + CLIENT_ID
+
+               self.loader = rb.Loader()
+               if ct['tracks-type'] == 'playlist':
+                       self.loader.get_url(trackurl, self.playlist_api_cb)
+               else:
+                       self.loader.get_url(trackurl, self.search_tracks_api_cb)
+
+       def sort_order_changed_cb(self, obj, pspec):
+               obj.resort_model()
+
+       def songs_selection_changed_cb(self, songs):
+               self.build_sc_menu()
+
+       def playing_entry_changed_cb(self, player, entry):
+               self.build_sc_menu()
+               if not entry:
+                       return
+               if entry.get_entry_type() != self.props.entry_type:
+                       return
+
+               au = entry.get_string(RB.RhythmDBPropType.MB_ALBUMID)
+               if au:
+                       key = RB.ExtDBKey.create_storage("title", entry.get_string(RB.RhythmDBPropType.TITLE))
+                       key.add_field("artist", entry.get_string(RB.RhythmDBPropType.ARTIST))
+                       self.art_store.store_uri(key, RB.ExtDBSourceType.EMBEDDED, au)
+
+       def open_uri_action_cb(self, action, param):
+               shell = self.props.shell
+               window = shell.props.window
+               screen = window.get_screen()
+
+               uri = param.get_string()
+               Gtk.show_uri(screen, uri, Gdk.CURRENT_TIME)
+
+       def build_sc_menu(self):
+               menu = {}
+
+               # playing track
+               shell = self.props.shell
+               player = shell.props.shell_player
+               entry = player.get_playing_entry()
+               if entry is not None and entry.get_entry_type() == self.props.entry_type:
+                       url = entry.get_string(RB.RhythmDBPropType.LOCATION)
+                       menu[url] = _("View '%(title)s' on SoundCloud") % {'title': 
entry.get_string(RB.RhythmDBPropType.TITLE) }
+                       # artist too?
+
+
+               # selected track
+               if self.songs.have_selection():
+                       entry = self.songs.get_selected_entries()[0]
+                       url = entry.get_string(RB.RhythmDBPropType.LOCATION)
+                       menu[url] = _("View '%(title)s' on SoundCloud") % {'title': 
entry.get_string(RB.RhythmDBPropType.TITLE) }
+                       # artist too?
+
+               # selected container
+               selection = self.container_view.get_selection()
+               (model, aiter) = selection.get_selected()
+               if aiter is not None:
+                       [name, url] = model.get(aiter, 0, 3)
+                       menu[url] = _("View '%(container)s' on SoundCloud") % {'container': name}
+
+               if len(menu) == 0:
+                       self.sc_button.set_menu_model(None)
+                       self.sc_button.set_sensitive(False)
+                       return None
+
+               m = Gio.Menu()
+               for u in menu:
+                       i = Gio.MenuItem()
+                       i.set_label(menu[u])
+                       i.set_action_and_target_value("win.soundcloud-open-uri", GLib.Variant.new_string(u))
+                       m.append_item(i)
+               self.sc_button.set_menu_model(m)
+               self.sc_button.set_sensitive(True)
+
+       def setup(self):
+               shell = self.props.shell
+
+               builder = Gtk.Builder()
+               builder.add_from_file(rb.find_plugin_file(self.props.plugin, "soundcloud.ui"))
+
+               self.scrolled = builder.get_object("container-scrolled")
+               self.scrolled.set_no_show_all(True)
+               self.scrolled.hide()
+
+               self.search_entry = RB.SearchEntry()
+               self.search_entry.props.explicit_mode = True
+
+               action = Gio.SimpleAction.new("soundcloud-search-type", GLib.VariantType.new('s'))
+               action.connect("activate", self.search_type_action_cb)
+               shell.props.window.add_action(action)
+
+               m = Gio.Menu()
+               for st in sorted(self.search_types):
+                       i = Gio.MenuItem()
+                       i.set_label(self.search_types[st]['label'])
+                       i.set_action_and_target_value("win.soundcloud-search-type", 
GLib.Variant.new_string(st))
+                       m.append_item(i)
+
+               self.search_popup = Gtk.Menu.new_from_model(m)
+
+               action.activate(GLib.Variant.new_string("tracks"))
+
+               grid = builder.get_object("soundcloud-source")
+
+               self.search_entry.connect("search", self.search_entry_cb)
+               self.search_entry.connect("activate", self.search_entry_cb)
+               self.search_entry.connect("show-popup", self.search_popup_cb)
+               self.search_entry.set_size_request(400, -1)
+               builder.get_object("search-box").pack_start(self.search_entry, False, True, 0)
+
+
+
+               self.search_popup.attach_to_widget(self.search_entry)
+
+               self.containers = builder.get_object("container-store")
+               self.container_view = builder.get_object("containers")
+               self.container_view.set_model(self.containers)
+
+               action = Gio.SimpleAction.new("soundcloud-open-uri", GLib.VariantType.new('s'))
+               action.connect("activate", self.open_uri_action_cb)
+               shell.props.window.add_action(action)
+
+               r = Gtk.CellRendererText()
+               c = Gtk.TreeViewColumn("", r, text=0)
+               self.container_view.append_column(c)
+
+               self.container_view.get_selection().connect('changed', self.selection_changed_cb)
+
+               self.songs = RB.EntryView(db=shell.props.db,
+                                         shell_player=shell.props.shell_player,
+                                         is_drag_source=True,
+                                         is_drag_dest=False)
+               self.songs.append_column(RB.EntryViewColumn.TITLE, True)
+               self.songs.append_column(RB.EntryViewColumn.ARTIST, True)
+               self.songs.append_column(RB.EntryViewColumn.DURATION, True)
+               self.songs.append_column(RB.EntryViewColumn.YEAR, False)
+               self.songs.append_column(RB.EntryViewColumn.GENRE, False)
+               self.songs.append_column(RB.EntryViewColumn.BPM, False)
+               self.songs.append_column(RB.EntryViewColumn.FIRST_SEEN, False)
+               self.songs.set_model(self.props.query_model)
+               self.songs.connect("notify::sort-order", self.sort_order_changed_cb)
+               self.songs.connect("selection-changed", self.songs_selection_changed_cb)
+
+               paned = builder.get_object("paned")
+               paned.pack2(self.songs)
+
+               self.bind_settings(self.songs, paned, None, True)
+
+               self.sc_button = Gtk.MenuButton()
+               self.sc_button.set_relief(Gtk.ReliefStyle.NONE)
+               img = Gtk.Image.new_from_file(rb.find_plugin_file(self.props.plugin, 
"powered-by-soundcloud.png"))
+               self.sc_button.add(img)
+               box = builder.get_object("soundcloud-button-box")
+               box.pack_start(self.sc_button, True, True, 0)
+
+               self.build_sc_menu()
+
+               self.pack_start(grid, expand=True, fill=True, padding=0)
+               grid.show_all()
+
+               self.art_store = RB.ExtDB(name="album-art")
+               player = shell.props.shell_player
+               player.connect('playing-song-changed', self.playing_entry_changed_cb)
+
+       def do_get_entry_view(self):
+               return self.songs
+
+       def do_get_playback_status(self, text, progress):
+               return self.get_progress()
+
+GObject.type_register(SoundCloudSource)
diff --git a/plugins/soundcloud/soundcloud.ui b/plugins/soundcloud/soundcloud.ui
new file mode 100644
index 0000000..7a62e11
--- /dev/null
+++ b/plugins/soundcloud/soundcloud.ui
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.6"/>
+  <object class="GtkListStore" id="container-store">
+    <columns>
+      <!-- column-name name -->
+      <column type="gchararray"/>
+      <!-- column-name type -->
+      <column type="gchararray"/>
+      <!-- column-name url -->
+      <column type="gchararray"/>
+      <!-- column-name permalink -->
+      <column type="gchararray"/>
+      <!-- column-name image -->
+      <column type="gchararray"/>
+      <!-- column-name description -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkGrid" id="soundcloud-source">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="margin_top">12</property>
+    <property name="vexpand">True</property>
+    <property name="row_spacing">12</property>
+    <child>
+      <object class="GtkPaned" id="paned">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkScrolledWindow" id="container-scrolled">
+            <property name="width_request">150</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkTreeView" id="containers">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <child internal-child="selection">
+                  <object class="GtkTreeSelection" id="treeview-selection1"/>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="resize">False</property>
+            <property name="shrink">True</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="box1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_right">6</property>
+        <child>
+          <object class="GtkBox" id="search-box">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="soundcloud-button-box">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 99db7e3..27f3911 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -150,6 +150,9 @@ plugins/replaygain/player.py
 [type: gettext/glade]plugins/replaygain/replaygain-prefs.ui
 [type: gettext/ini]plugins/sendto/sendto.plugin.in
 plugins/sendto/sendto.py
+[type: gettext/ini]plugins/soundcloud/soundcloud.plugin.in
+plugins/soundcloud/soundcloud.py
+[type: gettext/glade]plugins/soundcloud/soundcloud.ui
 plugins/visualizer/rb-visualizer-fullscreen.c
 plugins/visualizer/rb-visualizer-menu.c
 plugins/visualizer/rb-visualizer-page.c


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]