1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
|
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012 Tiger Soldier <tigersoldi@gmail.com>
#
# This file is part of OSD Lyrics.
#
# OSD Lyrics 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 3 of the License, or
# (at your option) any later version.
#
# OSD Lyrics 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 OSD Lyrics. If not, see <https://www.gnu.org/licenses/>.
#
import logging
import threading
import dbus
from .app import App
from .config import Config
from .consts import (LYRIC_SOURCE_PLUGIN_INTERFACE,
LYRIC_SOURCE_PLUGIN_OBJECT_PATH_PREFIX)
from .dbusext.service import Object as DBusObject, property as dbus_property
from .metadata import Metadata
SEARCH_SUCCEED = 0
SEARCH_CANCELLED = 1
SEARCH_FAILED = 2
DOWNLOAD_SUCCEED = 0
DOWNLOAD_CANCELLED = 1
DOWNLOAD_FAILED = 2
def onmainthread(func):
def decfunc(self, app, *args, **kwargs):
def timeout_cb():
func(self, *args, **kwargs)
return False
app.run_on_main_thread(timeout_cb)
return decfunc
class SearchResult:
""" Lyrics that match the metadata to be searched.
"""
def __init__(self, sourceid, downloadinfo, title='', artist='', album='', comment=''):
"""
Arguments:
- `title`: The matched lyric title.
- `artist`: The matched lyric artist.
- `album`: The matched lyric album.
- `downloadinfo`: Some additional data that is needed to download the
lyric. Normally this value is the url or ID of the lyric.
``downloadinfo`` MUST be composed with basic types such as numbers,
lists, dicts or strings so that it can be converted to D-Bus compatible
dict with `to_dict` method.
"""
self._title = title
self._artist = artist
self._album = album
self._comment = comment
self._sourceid = sourceid
self._downloadinfo = downloadinfo
def to_dict(self, ):
""" Convert the result to a dict so that it can be sent with D-Bus.
"""
return {'title': self._title,
'artist': self._artist,
'album': self._album,
'comment': self._comment,
'sourceid': self._sourceid,
'downloadinfo': self._downloadinfo}
class BaseTaskThread(threading.Thread):
""" Base thread for search or download tasks.
Plugins MUST provide a callable object as the `target` argument in the
initializer. The target does the task and returns the results. If task
fails, an Exception SHOULD be raised in the target.
"""
def __init__(self, onfinish, onerror, target, args=(), kwargs={}):
"""
Initialize the thread. The main thread should provide to callbacks
to notify the main thread that the thread is finished or an error
occurs. Please note that the two callbacks are run in the new thread,
not the main thread. So the callback may notify the main thread to
handle the results with App.run_on_main_thread.
Arguments:
- `onfinish`: A callable object to be invoked when `target` finish.
The callable SHOULD receive the values returned by `do_task`.
- `onerror`: A callable object to be invoked when `target` raise an
exception. The callable SHOULD receive the exception raised by
`do_task`.
- `target`: The callable object to be invoked by the run() method.
- `args`: The argument tuple for the target invocation. Defaults to `()`.
- `kwargs`: A dictionary of keyword arguments for the target invocation.
Defaults to `{}`.
"""
super().__init__()
self._onfinish = onfinish
self._onerror = onerror
self._args = args
self._kwargs = kwargs
self._target = target
def run(self):
""" Runs the task thread. Do NOT override this method. Override
`do_task` instead.
"""
try:
ret = self._target(*self._args, **self._kwargs)
self._onfinish(ret)
except Exception as e:
logging.exception('Got exception in thread')
self._onerror(e)
import sys
sys.stdout.flush()
class BaseLyricSourcePlugin(DBusObject):
""" Base class for implementing a lyric source plugin
"""
def __init__(self, id, name=None, watch_daemon=True):
"""
Create a new lyric source instance.
Arguments:
- `id`: The unique ID of the lyric source plugin. The full bus
name of the plugin will be `org.osdlyrics.LyricSourcePlugin.id`
- `name`: (optional) The name of the plugin, which should be properly
localized. If `name` is missing, the plugin will take `id` as its
name.
- `watch_daemon`: Whether to watch daemon bus.
"""
self._id = id
self._app = App('LyricSourcePlugin.' + id,
watch_daemon=watch_daemon)
super().__init__(conn=self._app.connection,
object_path=LYRIC_SOURCE_PLUGIN_OBJECT_PATH_PREFIX + self._id)
self._search_count = 0
self._download_count = 0
self._search_tasks = {}
self._download_tasks = {}
self._name = name if name is not None else id
self._config = None
def do_search(self, metadata):
"""
Do the real search work by plugins. All plugins MUST implement this method.
This method runs in a seperate thread, so don't worry about block IO.
Parameters:
- `metadata`: The metadata of the track to search. The type of `metadata`
is osdlyrics.metadata.Metadata
Returns: A list of SearchResult objects
"""
raise NotImplementedError()
@onmainthread
def do_searchsuccess(self, ticket, results):
if ticket in self._search_tasks:
del self._search_tasks[ticket]
dbusresults = [result.to_dict() for result in results]
self.SearchComplete(ticket, SEARCH_SUCCEED, dbusresults)
@onmainthread
def do_searchfailure(self, ticket, e):
if ticket in self._search_tasks:
del self._search_tasks[ticket]
logging.info('Search fail, %s', e)
self.SearchComplete(ticket, SEARCH_FAILED, [])
@dbus.service.method(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
in_signature='a{sv}',
out_signature='i')
def Search(self, metadata):
ticket = self._search_count
self._search_count = self._search_count + 1
thread = BaseTaskThread(onfinish=lambda result: self.do_searchsuccess(self._app, ticket, result),
onerror=lambda e: self.do_searchfailure(self._app, ticket, e),
target=self.do_search,
kwargs={'metadata': Metadata.from_dict(metadata)})
self._search_tasks[ticket] = thread
thread.start()
return ticket
@dbus.service.method(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
in_signature='i',
out_signature='')
def CancelSearch(self, ticket):
if ticket in self._search_tasks:
del self._search_tasks[ticket]
self.SearchComplete(ticket, SEARCH_CANCELLED, [])
def do_download(self, downloadinfo):
"""
Do the real download work by plugins. All plugins MUST implement this
method.
This method runs in a seperate thread, so don't worry about block IO.
Parameters:
- `downloadinfo`: The additional info taken from `downloadinfo` field in
SearchResult objects.
Returns: A string of the lyric content
"""
raise NotImplementedError()
@onmainthread
def do_downloadsuccess(self, ticket, content):
if ticket in self._download_tasks:
del self._download_tasks[ticket]
self.DownloadComplete(ticket, DOWNLOAD_SUCCEED, content)
@onmainthread
def do_downloadfailure(self, ticket, e):
if ticket in self._download_tasks:
del self._download_tasks[ticket]
self.DownloadComplete(ticket, DOWNLOAD_FAILED, str(e).encode('utf-8'))
@dbus.service.method(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
in_signature='v',
out_signature='i')
def Download(self, downloadinfo):
ticket = self._download_count
self._download_count = self._download_count + 1
thread = BaseTaskThread(onfinish=lambda content: self.do_downloadsuccess(self._app, ticket, content),
onerror=lambda e: self.do_downloadfailure(self._app, ticket, e),
target=self.do_download,
kwargs={'downloadinfo': downloadinfo})
self._download_tasks[ticket] = thread
thread.start()
return ticket
@dbus.service.method(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
in_signature='i',
out_signature='')
def CancelDownload(self, ticket):
if ticket in self._download_tasks:
del self._download_tasks[ticket]
self.DownloadComplete(ticket, DOWNLOAD_CANCELLED, '')
@dbus_property(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
type_signature='s')
def Name(self):
return self._name
@dbus.service.signal(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
signature='iiaa{sv}')
def SearchComplete(self, ticket, status, results):
logging.debug('search complete: ticket: %d, status: %d', ticket, status)
pass
@dbus.service.signal(dbus_interface=LYRIC_SOURCE_PLUGIN_INTERFACE,
signature='iiay')
def DownloadComplete(self, ticket, status, result):
logging.debug('download complete: ticket: %d, status: %d%s', ticket, status, '' if status == DOWNLOAD_SUCCEED else ', result: %s' % result)
pass
def run(self):
"""
Run the plugin as a standalone application
"""
self._app.run()
@property
def app(self):
"""
Return the App object that the plugin uses.
"""
return self._app
@property
def id(self):
"""
Return the ID of the lyric source
"""
return self._id
@property
def config_proxy(self):
if self._config is None:
self._config = Config(self._app.connection)
return self._config
def test():
class DummyLyricSourcePlugin(BaseLyricSourcePlugin):
def __init__(self):
super().__init__(id='dummy', watch_daemon=False)
def do_search(self, metadata):
if metadata.title:
logging.info('title: %s', metadata.title)
results = [SearchResult(title=metadata.title + str(i),
artist=metadata.artist + str(i),
album=metadata.album + str(i),
sourceid=i,
downloadinfo='\n'.join((metadata.title,
metadata.artist,
metadata.album)))
for i in range(10)]
return results
raise Exception('Title must not be empty')
def do_download(self, downloadinfo):
return b'dummy-downloaded-content'
search_tickets = {}
download_tickets = {}
def search_reply(ticket, expect_status):
if ticket in search_tickets:
logging.warning('Error: search ticket %d exists', ticket)
else:
search_tickets[ticket] = expect_status
def search_complete_cb(ticket, status, results):
if ticket not in search_tickets:
logging.warning('Error! search ticket not exists')
return
if search_tickets[ticket] != status:
logging.warning('Error! expect search %d with status %d but %d got',
ticket, search_tickets[ticket], status)
return
logging.debug('Search #%d with status %d', ticket, status)
if status == 0:
downloadinfo = results[0]['downloadinfo']
source.Download(downloadinfo,
reply_handler=lambda t: download_reply(t, 0),
error_handler=dummy_error)
del search_tickets[ticket]
check_quit()
def download_reply(ticket, expect_status):
if ticket in download_tickets:
logging.warning('Error: download ticket %d already exists', ticket)
else:
download_tickets[ticket] = expect_status
def download_complete_cb(ticket, status, content):
if ticket not in download_tickets:
logging.warning('Error! download ticket not exists')
return
if download_tickets[ticket] != status:
logging.warning('Error! expect download status %d but %d got', download_tickets[ticket], status)
return
if status == 0:
logging.debug('Download #%d success', ticket)
logging.debug('Downloaded content: \n%s', ''.join([chr(b) for b in content]))
else:
logging.warning('Download #%d fail, msg: %s', ticket, ''.join([chr(b) for b in content]))
del download_tickets[ticket]
check_quit()
wait_count = [4]
def check_quit():
wait_count[0] -= 1
if wait_count[0] == 0:
app.quit()
def dummy_error(e):
logging.warning('Error: ' + e)
dummysource = DummyLyricSourcePlugin()
app = dummysource.app
conn = app.connection
source = conn.get_object('org.osdlyrics.LyricSourcePlugin.dummy',
'/org/osdlyrics/LyricSourcePlugin/dummy')
source.connect_to_signal('SearchComplete',
search_complete_cb)
source.connect_to_signal('DownloadComplete',
download_complete_cb)
source.Search({'title': 'dummytitle',
'artist': 'dummyartist',
'album': 'dummyalbum'},
reply_handler=lambda t: search_reply(t, 0),
error_handler=dummy_error)
source.Search({'foo': 'bar'},
reply_handler=lambda t: search_reply(t, 2),
error_handler=dummy_error)
source.Download(123,
reply_handler=lambda t: download_reply(t, 2),
error_handler=dummy_error)
app.run()
if __name__ == '__main__':
test()
|