objects.py 19.5 KB
Newer Older
Cédric Bellegarde's avatar
Cédric Bellegarde committed
1
# Copyright (c) 2014-2019 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
2 3 4 5 6 7 8 9 10 11 12 13
# Copyright (c) 2015 Jean-Philippe Braun <eon@patapon.info>
# 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 3 of the License, or
# (at your option) any later 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, see <http://www.gnu.org/licenses/>.

14 15 16
from gi.repository import GLib, Gio

import json
17

18
from urllib.parse import urlparse
19
from lollypop.radios import Radios
20
from lollypop.logger import Logger
21
from lollypop.define import App, Type
22
from lollypop.utils import remove_static, escape
23 24 25


class Base:
26 27 28
    """
        Base for album and track objects
    """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
29

30
    def __init__(self, db):
31 32 33
        self.db = db

    def __dir__(self, *args, **kwargs):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
34
        """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
35
            Concatenate base class"s fields with child class"s fields
Cédric Bellegarde's avatar
Cédric Bellegarde committed
36
        """
37 38
        return super(Base, self).__dir__(*args, **kwargs) +\
            list(self.DEFAULTS.keys())
39

40 41 42 43 44
    # Used by pickle
    def __getstate__(self): return self.__dict__

    def __setstate__(self, d): self.__dict__.update(d)

45
    def __getattr__(self, attr):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
46
        # Lazy DB calls of attributes
47
        if attr in list(self.DEFAULTS.keys()):
48
            if self.id is None or self.id < 0:
49
                return self.DEFAULTS[attr]
Cédric Bellegarde's avatar
Cédric Bellegarde committed
50
            # Actual value of "attr_name" is stored in "_attr_name"
51 52
            attr_name = "_" + attr
            attr_value = getattr(self, attr_name)
53
            if attr_value is None:
54
                attr_value = getattr(self.db, "get_" + attr)(self.id)
55
                setattr(self, attr_name, attr_value)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
56 57
            # Return default value if None
            if attr_value is None:
58
                return self.DEFAULTS[attr]
Cédric Bellegarde's avatar
Cédric Bellegarde committed
59 60
            else:
                return attr_value
61

62 63 64 65 66 67 68 69 70
    def reset(self, attr):
        """
            Reset attr
            @param attr as str
        """
        attr_name = "_" + attr
        attr_value = getattr(self.db, "get_" + attr)(self.id)
        setattr(self, attr_name, attr_value)

71
    @property
72
    def is_in_user_collection(self):
73
        """
74
            True if track is in user collection
75 76
            @return bool
        """
77
        return self.mtime > 0
78

79
    def get_popularity(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
80 81 82
        """
            Get popularity
            @return int between 0 and 5
83
        """
84
        if self.id is None:
85
            return 0
86 87

        popularity = 0
88 89 90 91 92 93 94 95
        if self.id >= 0:
            avg_popularity = self.db.get_avg_popularity()
            if avg_popularity > 0:
                popularity = self.db.get_popularity(self.id)
        elif self.id == Type.RADIOS:
            radios = Radios()
            avg_popularity = radios.get_avg_popularity()
            if avg_popularity > 0:
96
                popularity = radios.get_popularity(self._radio_id)
97 98
        return popularity * 5 / avg_popularity + 0.5

99
    def set_popularity(self, new_rate):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
100 101
        """
            Set popularity
102
            @param new_rate as int between 0 and 5
103
        """
104
        if self.id is None:
105
            return
106
        try:
107 108
            if self.id >= 0:
                avg_popularity = self.db.get_avg_popularity()
109 110 111 112
                popularity = int((new_rate * avg_popularity / 5) + 0.5)
                best_popularity = self.db.get_higher_popularity()
                if new_rate == 5:
                    popularity = (popularity + best_popularity) / 2
113
                self.db.set_popularity(self.id, popularity)
114 115 116
            elif self.id == Type.RADIOS:
                radios = Radios()
                avg_popularity = radios.get_avg_popularity()
117 118 119 120
                popularity = int((new_rate * avg_popularity / 5) + 0.5)
                best_popularity = self.db.get_higher_popularity()
                if new_rate == 5:
                    popularity = (popularity + best_popularity) / 2
121
                radios.set_popularity(self._radio_id, popularity)
122
        except Exception as e:
123
            Logger.error("Base::set_popularity(): %s" % e)
124

125 126 127 128 129
    def get_rate(self):
        """
            Get rate
            @return int
        """
130
        if self.id is None:
131 132 133 134
            return 0

        rate = 0
        if self.id >= 0:
Cédric Bellegarde's avatar
Cédric Bellegarde committed
135
            rate = self.db.get_rate(self.id)
136 137
        elif self.id == Type.RADIOS:
            radios = Radios()
138
            rate = radios.get_rate(self._radio_id)
139 140 141 142 143 144 145 146 147
        return rate

    def set_rate(self, rate):
        """
            Set rate
            @param rate as int between -1 and 5
        """
        if self.id == Type.RADIOS:
            radios = Radios()
148
            radios.set_rate(self._radio_id, rate)
149
            App().player.emit("rate-changed", self._radio_id, rate)
150 151
        else:
            self.db.set_rate(self.id, rate)
152
            App().player.emit("rate-changed", self.id, rate)
153

154

155 156 157 158 159
class Disc:
    """
        Represent an album disc
    """

160
    def __init__(self, album, disc_number, disallow_ignored_tracks):
161
        self.db = App().albums
162 163 164
        self.__tracks = []
        self.__album = album
        self.__number = disc_number
165
        self.__disallow_ignored_tracks = disallow_ignored_tracks
166

167 168 169 170 171 172 173
    def set_tracks(self, tracks):
        """
            Set disc tracks
            @param tracks as [Track]
        """
        self.__tracks = tracks

174
    @property
175
    def number(self):
176
        """
177
            Get disc number
178
        """
179 180 181 182 183 184 185 186 187
        return self.__number

    @property
    def album(self):
        """
            Get disc album
            @return Album
        """
        return self.__album
188

189
    @property
190
    def track_ids(self):
191
        """
192 193 194 195
            Get disc track ids
            @return [int]
        """
        return [track.id for track in self.tracks]
196

197 198 199 200 201 202 203 204
    @property
    def track_uris(self):
        """
            Get disc track uris
            @return [str]
        """
        return [track.uri for track in self.tracks]

205 206 207
    @property
    def tracks(self):
        """
208 209
            Get disc tracks
            @return [Track]
210
        """
211
        if not self.__tracks and self.album.id is not None:
212
            artist_ids = remove_static(self.album.artist_ids)
213 214
            self.__tracks = [Track(track_id, self.album)
                             for track_id in self.db.get_disc_track_ids(
Cédric Bellegarde's avatar
Cédric Bellegarde committed
215 216
                self.album.id,
                self.album.genre_ids,
217
                artist_ids,
218 219
                self.number,
                self.__disallow_ignored_tracks)]
220
        return self.__tracks
221 222


223
class Album(Base):
224 225 226
    """
        Represent an album
    """
227
    DEFAULTS = {"name": "",
228
                "artists": [],
229
                "artist_ids": [],
Cédric Bellegarde's avatar
Cédric Bellegarde committed
230
                "year": None,
231
                "timestamp": None,
232
                "uri": "",
Cédric Bellegarde's avatar
Cédric Bellegarde committed
233
                "tracks_count": 1,
234
                "duration": 0,
235
                "popularity": 0,
236
                "mtime": 1,
237
                "synced": False,
238 239
                "loved": False,
                "mb_album_id": None}
240

241 242
    def __init__(self, album_id=None, genre_ids=[], artist_ids=[],
                 disallow_ignored_tracks=False):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
243 244 245
        """
            Init album
            @param album_id as int
246
            @param genre_ids as [int]
247
            @param disallow_ignored_tracks as bool
248
        """
249
        Base.__init__(self, App().albums)
250
        self.id = album_id
251
        self.genre_ids = remove_static(genre_ids)
252
        self._tracks = []
253
        self._discs = []
254
        self.__disallow_ignored_tracks = disallow_ignored_tracks
255
        self.__one_disc = None
256 257 258
        # Use artist ids from db else
        if artist_ids:
            self.artist_ids = artist_ids
259

Cédric Bellegarde's avatar
Cédric Bellegarde committed
260
    def clone(self, disallow_ignored_tracks):
261
        """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
262 263
            Clone album
            @param disallow_ignored_tracks as bool
264
        """
265 266
        album = Album(self.id, self.genre_ids,
                      self.artist_ids, disallow_ignored_tracks)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
267 268
        if not disallow_ignored_tracks:
            album.set_tracks(self.tracks)
269
        return album
270

271 272 273 274 275 276 277
    def set_discs(self, discs):
        """
            Set album discs
            @param discs as [Disc]
        """
        self._discs = discs

278 279
    def set_tracks(self, tracks):
        """
280
            Set album tracks (cloned tracks)
281 282
            @param tracks as [Track]
        """
283 284 285 286
        self._tracks = []
        for track in tracks:
            new_track = Track(track.id, self)
            self._tracks.append(new_track)
287

Cédric Bellegarde's avatar
Cédric Bellegarde committed
288
    def insert_track(self, track, position=-1):
289
        """
290
            Add track to album (cloned track)
291
            @param track as Track
Cédric Bellegarde's avatar
Cédric Bellegarde committed
292
            @param position as int
293
        """
294
        new_track = Track(track.id, self)
295
        if position == -1:
296
            self._tracks.append(new_track)
297
        else:
298
            self._tracks.insert(position, new_track)
299

300
    def remove_track(self, track):
301 302
        """
            Remove track from album
303
            @param track as Track
304
            @return True if album empty
305
        """
306 307
        if track in self.tracks:
            self._tracks.remove(track)
308
        return len(self._tracks) == 0
309

310 311 312 313 314 315
    def clear_tracks(self):
        """
            Clear album tracks
        """
        self._tracks = []

316 317 318 319 320 321 322 323
    def disc_names(self, disc):
        """
            Disc names
            @param disc as int
            @return disc names as [str]
        """
        return self.db.get_disc_names(self.id, disc)

324 325 326 327 328 329 330 331 332
    def set_loved(self, loved):
        """
            Mark album as loved
            @param loved as bool
        """
        if self.id >= 0:
            App().albums.set_loved(self.id, loved)
            self.loved = loved

333 334 335 336 337 338 339
    def set_uri(self, uri):
        """
            Set album uri
            @param uri as str
        """
        if self.id >= 0:
            App().albums.set_uri(self.id, uri)
340
        self.uri = uri
341

342 343 344 345
    def get_track(self, track_id):
        """
            Get track
            @param track_id as int
346
            @return Track
347
        """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
348
        for track in self.tracks:
349 350
            if track.id == track_id:
                return track
351
        return Track()
352

Cédric Bellegarde's avatar
Cédric Bellegarde committed
353 354 355 356 357 358 359 360 361 362 363
    def save(self, save):
        """
            Save album to collection
            @param save as bool
        """
        if save:
            App().albums.set_mtime(self.id, -1)
        else:
            App().albums.set_mtime(self.id, 0)
        for track in self.tracks:
            track.save(save)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
364 365 366
        self.reset("mtime")
        for artist_id in self.artist_ids:
            App().scanner.emit("artist-updated", artist_id, save)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
367
        App().scanner.emit("album-updated", self.id, save)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
368

369 370 371 372 373 374 375 376 377
    @property
    def synced(self):
        """
            Get synced state
            Remove from cache
            @return int
        """
        return App().albums.get_synced(self.id)

378 379
    @property
    def title(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
380 381 382 383
        """
            Get album name
            @return str
        """
384 385 386
        return self.name

    @property
Cédric Bellegarde's avatar
Cédric Bellegarde committed
387
    def track_ids(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
388
        """
389
            Get album track ids
390
            @return [int]
391
        """
392
        return [track.id for track in self.tracks]
393

394 395 396 397 398 399 400 401
    @property
    def track_uris(self):
        """
            Get album track uris
            @return [str]
        """
        return [track.uri for track in self.tracks]

402 403
    @property
    def tracks(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
404 405
        """
            Get album tracks
406
            @return [Track]
407
        """
408
        if not self._tracks and self.id is not None:
409 410
            for disc in self.discs:
                self._tracks += disc.tracks
411 412
        return self._tracks

413 414 415 416 417 418 419 420
    @property
    def one_disc(self):
        """
            Get album as one disc
            @return Disc
        """
        if self.__one_disc is None:
            tracks = self.tracks
421
            self.__one_disc = Disc(self, 0, self.__disallow_ignored_tracks)
422 423 424
            self.__one_disc.set_tracks(tracks)
        return self.__one_disc

425 426 427 428
    @property
    def discs(self):
        """
            Get albums discs
429
            @return [Disc]
430 431
        """
        if not self._discs:
432
            disc_numbers = self.db.get_discs(self.id, self.genre_ids)
433 434
            self._discs = [Disc(self, number, self.__disallow_ignored_tracks)
                           for number in disc_numbers]
435
        return self._discs
436

437 438

class Track(Base):
439 440 441
    """
        Represent a track
    """
442 443 444 445 446 447
    DEFAULTS = {"name": "",
                "album_id": None,
                "artist_ids": [],
                "genre_ids": [],
                "popularity": 0,
                "album_name": "",
448 449
                "artists": [],
                "genres": [],
450 451
                "duration": 0,
                "number": 0,
452 453
                "discnumber": 0,
                "discname": "",
454
                "year": None,
455
                "timestamp": None,
456
                "mtime": 1,
457
                "loved": False,
458 459
                "mb_track_id": None,
                "mb_artist_ids": []}
460

461
    def __init__(self, track_id=None, album=None):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
462 463 464
        """
            Init track
            @param track_id as int
465
            @param album as Album
466
        """
467
        Base.__init__(self, App().tracks)
468
        self.id = track_id
469 470
        self._radio_id = None
        self._radio_name = ""
Cédric Bellegarde's avatar
Cédric Bellegarde committed
471
        self._uri = None
472
        self._number = 0
473 474 475 476 477

        if album is None:
            self.__album = Album(self.album_id)
        else:
            self.__album = album
478

479 480 481 482 483 484 485
    def set_album(self, album):
        """
            Set track album
            @param album as Album
        """
        self.__album = album

486 487 488 489 490 491 492 493 494
    def set_uri(self, uri):
        """
            Set uri
            @param uri as string
        """
        self._uri = uri

    def set_radio(self, name, uri):
        """
495
            Set radio for non DB radios (Tunein)
496 497 498
            @param name as string
            @param uri as string
        """
499 500
        from lollypop.radios import Radios
        radios = Radios()
501
        self.id = Type.RADIOS
502 503 504
        self._radio_id = radios.get_id(name)
        self._radio_name = name
        self._uri = uri
505 506 507 508 509
        # Generate a tmp album id, needed by InfoController
        album_id = 0
        for i in list(map(ord, name)):
            album_id += i
        self.album.id = album_id
510 511 512 513 514 515 516 517 518 519 520

    def set_radio_id(self, radio_id):
        """
            Set radio id
            @param radio_id as int
        """
        from lollypop.radios import Radios
        radios = Radios()
        name = radios.get_name(radio_id)
        uri = radios.get_uri(radio_id)
        self.set_radio(name, uri)
521

522 523 524 525 526 527 528
    def set_number(self, number):
        """
            Set number
            @param number as int
        """
        self._number = number

529 530
    def set_loved(self, loved):
        """
531 532
            Mark album as loved
            @param loved as bool
533
        """
534 535 536
        if self.id >= 0:
            App().tracks.set_loved(self.id, loved)
            self.loved = loved
537

Cédric Bellegarde's avatar
Cédric Bellegarde committed
538 539 540
    def save(self, save):
        """
            Save track to collection
541
            Cache it to Web Collection (for restore on reset)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
542 543
            @param save as bool
        """
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
        try:
            filename = "%s_%s_%s" % (self.album.name, self.artists, self.name)
            filepath = "%s/%s.txt" % (App().scanner._WEB_COLLECTION,
                                      escape(filename))
            f = Gio.File.new_for_path(filepath)
            if save:
                App().tracks.set_mtime(self.id, -1)
                data = {
                    "title": self.name,
                    "album_name": self.album.name,
                    "artists": self.artists,
                    "album_artists": self.album.artists,
                    "album_loved": self.album.loved,
                    "album_popularity": self.album.popularity,
                    "album_rate": self.album.get_rate(),
                    "discnumber": self.discnumber,
                    "discname": self.discname,
                    "duration": self.duration,
                    "tracknumber": App().tracks.get_number(self.id),
                    "track_popularity": self.popularity,
                    "track_loved": self.loved,
                    "track_rate": self.get_rate(),
                    "year": self.year,
                    "timestamp": self.timestamp,
                    "uri": self.uri
                }
                content = json.dumps(data).encode("utf-8")
                fstream = f.replace(None, False,
                                    Gio.FileCreateFlags.REPLACE_DESTINATION,
                                    None)
                if fstream is not None:
                    fstream.write(content, None)
                    fstream.close()
            else:
                App().tracks.set_mtime(self.id, 0)
                f.delete()
            self.reset("mtime")
        except Exception as e:
            Logger.error("Track::save(): %s", e)
Cédric Bellegarde's avatar
Cédric Bellegarde committed
583

584
    def get_featuring_artist_ids(self, album_artist_ids):
585 586 587 588
        """
            Get featuring artist ids
            @return [int]
        """
589 590
        artist_ids = self.db.get_artist_ids(self.id)
        return list(set(artist_ids) - set(album_artist_ids))
591

592 593 594 595 596 597 598 599
    @property
    def is_web(self):
        """
            True if track is a web track
            @return bool
        """
        return self.is_http or self.uri.startswith("web:")

600 601 602 603 604 605
    @property
    def is_http(self):
        """
            True if track is a http track
            @return bool
        """
606 607
        parsed = urlparse(self.uri)
        return parsed.scheme in ["http", "https"]
608

609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
    @property
    def position(self):
        """
            Get track position for album
            @return int
        """
        i = 0
        for track_id in self.__album.track_ids:
            if track_id == self.id:
                break
            i += 1
        return i

    @property
    def first(self):
        """
            Is track first for album
            @return bool
        """
        tracks = self.__album.tracks
        return tracks and self.id == tracks[0].id

    @property
    def last(self):
        """
            Is track last for album
            @return bool
        """
        tracks = self.__album.tracks
        return tracks and self.id == tracks[-1].id

640 641
    @property
    def title(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
642 643 644
        """
            Get track name
            Alias to Track.name
645 646 647
        """
        return self.name

648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
    @property
    def radio_id(self):
        """
            Get radio id
            @return int
        """
        return self._radio_id

    @property
    def radio_name(self):
        """
            Get radio name
            @return str
        """
        return self._radio_name

664 665 666
    @property
    def uri(self):
        """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
667 668 669
            Get track file uri
            @return str
        """
670
        if self._uri is None:
671
            self._uri = App().tracks.get_uri(self.id)
672
        return self._uri
673 674

    @property
675
    def path(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
676 677 678 679
        """
            Get track file path
            Alias to Track.path
            @return str
680
        """
681
        return GLib.filename_from_uri(self.uri)[0]
682 683 684

    @property
    def album(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
685
        """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
686
            Get track"s album
687
            @return Album
688
        """
689 690
        if self.__album is None:
            self.__album = Album(self._album_id)
691
        return self.__album
692 693

    @property
694
    def album_artists(self):
Cédric Bellegarde's avatar
Cédric Bellegarde committed
695
        """
Cédric Bellegarde's avatar
Cédric Bellegarde committed
696
            Get track album artists, can be != than album.artists as track
697
            may not have any album
Cédric Bellegarde's avatar
Cédric Bellegarde committed
698
            @return str
699
        """
700
        if getattr(self, "_album_artists") is None:
Cédric Bellegarde's avatar
Cédric Bellegarde committed
701
            self._album_artists = self.album.artists
702
        return self._album_artists