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
|
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Tiger Soldier
#
# 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 re
import dbus
from .consts import METADATA_ALBUM, METADATA_ARTIST, METADATA_TITLE
class Metadata:
"""
Metadata of a track
This class helps to deal with different metadata formats defined by MPRIS1,
MPRIS2 and OSD Lyrics. It is recommended to parse a metadata dict from D-Bus
with `Metadata.from_dict()`.
Metadata provides following properties: `title`, `artist`, `album`, `location`,
`arturl`, `length`, and `tracknum`, where `length` and `tracknum` are integers,
the others are strings.
"""
# Possible MPRIS metadata keys, taken from
# http://xmms2.org/wiki/MPRIS_Metadata#MPRIS_v1.0_Metadata_guidelines"""
MPRIS1_KEYS = set(['genre', 'comment', 'rating', 'year', 'date', 'asin',
'puid fingerprint', 'mb track id', 'mb artist id',
'mb artist sort name', 'mb album id', 'mb release date',
'mb album artist', 'mb album artist id',
'mb album artist sort name', 'audio-bitrate',
'audio-samplerate', 'video-bitrate'])
# Possible MPRIS2 metadata keys, taken from
# http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
MPRIS2_KEYS = set(['xesam:albumArtist', 'xesam:asText', 'xesam:audioBPM',
'xesam:autoRating', 'xesam:comment', 'xesam:composer',
'xesam:contentCreated', 'xesam:discNumber', 'xesam:firstUsed',
'xesam:genre', 'xesam:lastUsed', 'xesam:lyricist',
'xesam:useCount', 'xesam:userRating'])
def __init__(self,
title=None,
artist=None,
album=None,
arturl=None,
tracknum=-1,
location=None,
length=-1,
extra={}):
"""
Create a new Metadata instance.
Arguments:
- `title`: (string) The title of the track
- `artist`: (string) The artist of the track
- `album`: (string) The name of album that the track is in
- `arturl`: (string) The URI of the picture of the cover of the album
- `tracknum`: (int) The number of the track
- `location`: (string) The URI of the file
- `length`: (int) The duration of the track in milliseconds.
- `extra`: (dict) A dict that is intend to store additional properties
provided by MPRIS1 or MPRIS2 DBus dicts. The MPRIS1-related
values will be set in the dict returned by `to_mpris1`. The
MPRIS2-related values are treated in a similar way.
"""
self.title = title
self.artist = artist
self.album = album
self.arturl = arturl
self.tracknum = tracknum
self.location = location
self.length = length
self._extra = extra
def __eq__(self, other):
"""
Two metadatas are equal if:
- The locations are not empty and are equal, or
- The titles, artists and albums are equal.
See also: src/ol_metadata.c:ol_metadata_equal, thougn they aren't consistent.
"""
if self is other:
return True
if self.location == other.location and self.location != '':
return True
for key in [METADATA_TITLE, METADATA_ARTIST, METADATA_ALBUM]:
if getattr(self, key) != getattr(other, key):
return False
return True
def to_mpris1(self):
"""
Converts the metadata to mpris1 dict
"""
ret = dbus.Dictionary(signature='sv')
for k in ['title', 'artist', 'album', 'arturl', 'location']:
if getattr(self, k) is not None:
ret[k] = dbus.String(getattr(self, k))
if self.tracknum >= 0:
ret['tracknumber'] = dbus.String(self.tracknum)
if self.length >= 0:
ret['time'] = dbus.UInt32(self.length // 1000)
ret['mtime'] = dbus.UInt32(self.length)
for k, v in self._extra.items():
if k in self.MPRIS1_KEYS and k not in ret:
ret[k] = v
return ret
def to_mpris2(self):
"""
Converts the metadata to mpris2 dict
>>> mt = Metadata(title='Title', artist='Artist1, Artist2,Artist3',
... album='Album', arturl='file:///art/url',
... location='file:///path/to/file', length=123,
... tracknum=456,
... extra={ 'title': 'Fake Title',
... 'xesam:album': 'Fake Album',
... 'xesam:useCount': 780,
... 'xesam:userRating': 1.0,
... 'custom value': 'yoooooo',
... })
>>> dict = mt.to_mpris2()
>>> print(dict['xesam:title'])
Title
>>> print(dict['xesam:artist'])
[dbus.String('Artist1'), dbus.String('Artist2'), dbus.String('Artist3')]
>>> print(dict['xesam:url'])
file:///path/to/file
>>> print(dict['mpris:artUrl'])
file:///art/url
>>> print(dict['mpris:length'])
123
>>> print(dict['xesam:trackNumber'])
456
>>> print(dict['xesam:userRating'])
1.0
>>> 'custom value' in dict
False
>>> mt2 = Metadata.from_dict(dict)
>>> print(mt2.title)
Title
>>> print(mt2.artist)
Artist1, Artist2, Artist3
>>> print(mt2.album)
Album
>>> print(mt2.location)
file:///path/to/file
"""
ret = dbus.Dictionary(signature='sv')
mpris2map = {'title': 'xesam:title',
'album': 'xesam:album',
'arturl': 'mpris:artUrl',
'location': 'xesam:url',
}
for k in ['title', 'album', 'arturl', 'location']:
if getattr(self, k) is not None:
ret[mpris2map[k]] = dbus.String(getattr(self, k))
if self.artist is not None:
ret['xesam:artist'] = [dbus.String(v.strip()) for v in self.artist.split(',')]
if self.length >= 0:
ret['mpris:length'] = dbus.Int64(self.length)
if self.tracknum >= 0:
ret['xesam:trackNumber'] = dbus.Int32(self.tracknum)
for k, v in self._extra.items():
if k in self.MPRIS2_KEYS and k not in ret:
ret[k] = v
return ret
@classmethod
def from_mpris2(cls, mpris2_dict):
"""
Create a Metadata object from mpris2 metadata dict
"""
string_dict = {'title': 'xesam:title',
'album': 'xesam:album',
'arturl': 'mpris:artUrl',
'location': 'xesam:url',
}
string_list_dict = {'artist': 'xesam:artist'}
kargs = {}
for k, v in string_dict.items():
if v in mpris2_dict:
kargs[k] = mpris2_dict[v]
for k, v in string_list_dict.items():
if v in mpris2_dict:
kargs[k] = ', '.join(mpris2_dict[v])
if 'xesam:trackNumber' in mpris2_dict:
kargs['tracknum'] = int(mpris2_dict['xesam:trackNumber'])
if 'mpris:length' in mpris2_dict:
kargs['length'] = int(mpris2_dict['mpris:length'])
kargs['extra'] = mpris2_dict
return cls(**kargs)
@classmethod
def from_dict(cls, dbusdict):
"""
Create a Metadata object from a D-Bus dict object.
The D-Bus dict object can be MPRIS1 metadata or MPRIS2 metadata format. If
the dict both compatable with MPRIS1 and MPRIS2, MPRIS1 will be used.
>>> title = 'Title'
>>> artist = 'Artist'
>>> arturl = 'file:///art/url'
>>> location = 'file:///location'
>>> tracknumber = 42
>>> md1 = Metadata.from_dict({'title': title,
... 'artist': artist,
... 'arturl': arturl,
... 'location': location,
... 'tracknumber': str(tracknumber) + '/2'})
>>> md1.title == title
True
>>> md1.artist == artist
True
>>> md1.arturl == arturl
True
>>> md1.location == location
True
>>> md1.tracknum == tracknumber
True
>>> md2 = Metadata.from_dict({'xesam:title': title,
... 'xesam:artist': [artist],
... 'mpris:artUrl': arturl,
... 'xesam:url': location,
... 'xesam:trackNumber': tracknumber})
>>> md2.title == title
True
>>> md2.artist == artist
True
>>> md2.arturl == arturl
True
>>> md2.location == location
True
>>> md2.tracknum == tracknumber
True
>>> md3 = Metadata.from_dict({'title': title,
... 'artist': artist,
... 'arturl': arturl,
... 'location': location,
... 'tracknumber': str(tracknumber) + '/2',
... 'xesam:title': title + '1',
... 'xesam:artist': [artist + '1', '1'],
... 'mpris:artUrl': arturl + '1',
... 'xesam:url': location + '1',
... 'xesam:trackNumber': tracknumber + 1})
>>> md3.title == title
True
>>> md3.artist == artist
True
>>> md3.arturl == arturl
True
>>> md3.location == location
True
>>> md3.tracknum == tracknumber
True
>>> timedict = {'time': 10, 'mtime': 20, 'mpris:length': 3000}
>>> Metadata.from_dict(timedict).length
20
>>> del timedict['mtime']
>>> Metadata.from_dict(timedict).length
3
>>> del timedict['mpris:length']
>>> Metadata.from_dict(timedict).length
10000
"""
string_dict = {'title': ['title', 'xesam:title'],
'album': ['album', 'xesam:album'],
'arturl': ['arturl', 'mpris:artUrl'],
'artist': ['artist'],
'location': ['location', 'xesam:url'],
}
string_list_dict = {'artist': 'xesam:artist',
}
kargs = {}
for k, v in string_dict.items():
for dict_key in v:
if dict_key in dbusdict:
kargs[k] = dbusdict[dict_key]
break
# artist
for k, v in string_list_dict.items():
if k not in kargs and v in dbusdict:
kargs[k] = ', '.join(dbusdict[v])
# tracknumber
if 'tracknumber' in dbusdict:
tracknumber = dbusdict['tracknumber']
if isinstance(tracknumber, int):
# the specification requires tracknumber be a string. However,
# tracknumber in audacious is int32
kargs['tracknum'] = tracknumber
else:
if not re.match(r'\d+(/\d+)?', tracknumber):
logging.warning('Malfromed tracknumber: %s', tracknumber)
else:
kargs['tracknum'] = int(dbusdict['tracknumber'].split('/')[0])
if 'tracknum' not in kargs and 'xesam:trackNumber' in dbusdict:
kargs['tracknum'] = int(dbusdict['xesam:trackNumber'])
# length
if 'mtime' in dbusdict:
kargs['length'] = dbusdict['mtime']
elif 'mpris:length' in dbusdict:
kargs['length'] = dbusdict['mpris:length'] // 1000
elif 'time' in dbusdict:
kargs['length'] = dbusdict['time'] * 1000
kargs['extra'] = dbusdict
return cls(**kargs)
def __str__(self):
attrs = ['title', 'artist', 'album', 'location', 'length']
attr_value = [' %s: %s' % (key, getattr(self, key)) for key in attrs]
return 'metadata:\n' + '\n'.join(attr_value)
if __name__ == '__main__':
import doctest
doctest.testmod()
|