models.py 21.6 KB
Newer Older
1
#
David Trattnig's avatar
David Trattnig committed
2
# Aura Engine (https://gitlab.servus.at/aura/engine)
3
#
David Trattnig's avatar
David Trattnig committed
4
# Copyright (C) 2017-2020 - The Aura Engine Team.
5
#
David Trattnig's avatar
David Trattnig committed
6
7
8
9
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
10
#
David Trattnig's avatar
David Trattnig committed
11
12
13
14
# 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 Affero General Public License for more details.
15
#
David Trattnig's avatar
David Trattnig committed
16
17
18
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

19

20
import contextlib
21
import sys
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
22
import time
23
24
import logging
import datetime
25

26
27
import sqlalchemy as sa

28
import sqlalchemy
David Trattnig's avatar
David Trattnig committed
29
30
31
32
33
34
from sqlalchemy import BigInteger, Boolean, Column, DateTime, Integer, String, ForeignKey, ColumnDefault
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
35

David Trattnig's avatar
David Trattnig committed
36
37
from src.base.config import AuraConfig
from src.base.utils import SimpleUtil
David Trattnig's avatar
David Trattnig committed
38
from src.resources import ResourceUtil
David Trattnig's avatar
David Trattnig committed
39

David Trattnig's avatar
David Trattnig committed
40
# Initialize DB Model and session
David Trattnig's avatar
David Trattnig committed
41
config = AuraConfig()
David Trattnig's avatar
David Trattnig committed
42
engine = sa.create_engine(config.get_database_uri())
43
44
Base = declarative_base()
Base.metadata.bind = engine
45
46
__sqlalchemy_version = tuple(int(item) for item in sqlalchemy.__version__.split(".")[:2])

47
48

class DB():
David Trattnig's avatar
David Trattnig committed
49
50
    session_factory = sessionmaker(bind=engine)
    Session = scoped_session(session_factory)
51
52
53
    Model = Base


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# Monkey-patch the above DB.Session generator for SQLAlchemy before v1.4.
# Such older versions of SQLAlchemy do not support contexts.
if __sqlalchemy_version < (1, 4):
    @contextlib.contextmanager
    def get_session_context():
        """ provide a context for a session

        This context is the same as the one provided by a "scoped_session" in SQLAlchemy v1.4 or
        later.

        see https://docs.sqlalchemy.org/en/13/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it
        """
        session = scoped_session(DB.session_factory)
        try:
            yield session
        finally:
            session.close()

    DB.Session = get_session_context

74
75

class AuraDatabaseModel():
76
77
78
79
80
    """
    AuraDataBaseModel.

    Holding all tables and relationships for the engine.
    """
81
82
    logger = None

83

84
    def __init__(self):
85
86
87
        """
        Constructor.
        """
88
89
        self.logger = logging.getLogger("AuraEngine")

90

91
    def store(self, add=False, commit=False):
92
93
94
        """
        Store to the database
        """
95
96
97
98
99
100
101
        with DB.Session() as session:
            if add:
                session.add(self)
            else:
                session.merge(self)
            if commit:
                session.commit()
102

103

104
    def delete(self, commit=False):
105
106
107
        """
        Delete from the database
        """
108
109
110
111
        with DB.Session() as session:
            session.delete(self)
            if commit:
                session.commit()
112

113

114
115
116
117
    def refresh(self):
        """
        Refreshes the currect record
        """
118
119
120
        with DB.Session() as session:
            session.expire(self)
            session.refresh(self)
121
122


123
124
    def _asdict(self):
        return self.__dict__
125

126

127
128
129
    @staticmethod
    def init_database():
        """
130
        Initializes the database tables if they are not existing.
131
132
133
134
135
136
137

        Raises:
            sqlalchemy.exc.ProgrammingError:    In case the DB model is invalid
        """
        try:
            Playlist.is_empty()
        except sa.exc.ProgrammingError as e:
David Trattnig's avatar
David Trattnig committed
138
            is_available = True
139

David Trattnig's avatar
David Trattnig committed
140
141
142
143
144
145
146
147
            # PostgreSQL table not available
            if e.code == "f405":
                is_available = False
            # MariaDB table not available
            elif e.orig.args[0] == 1146:
                is_available = False

            if not is_available:
148
149
150
151
152
153
                model = AuraDatabaseModel()
                model.recreate_db()
            else:
                raise


154
155
    @staticmethod
    def recreate_db(systemexit = False):
156
        """
157
        Deletes all tables and re-creates the database.
David Trattnig's avatar
David Trattnig committed
158
        """
159
160
        Base.metadata.drop_all()
        Base.metadata.create_all()
161
162
        with DB.Session() as session:
            session.commit()
163

164
165
        if systemexit:
            sys.exit(0)
166

167

168
169

#
David Trattnig's avatar
David Trattnig committed
170
#   TIMESLOT
171
172
#

173

174
class Timeslot(DB.Model, AuraDatabaseModel):
175
    """
David Trattnig's avatar
David Trattnig committed
176
    One specific timeslot for a show.
177
178
179
180
181
182
183
184

    Relationships:
        playlist (Playlist):            The specific playlist for this timeslot
        schedule_default (Playlist):    Some playlist played by default, when no specific playlist is assigned
        show_default (Playlist):        Some playlist played by default, when no default schedule playlist is assigned
        schedule_fallback (Playlist):   Some playlist played as fallback, when no specific playlist is assigned or if it is errorneous (includes silence detection)
        show_fallback (Playlist):       Some playlist played as fallback, when no schedule fallback playlist is assigned or if some specific playlist is errorneous (includes silence detection)
        station_fallback (Playlist):    Defined in the original AURA API but not implemented, as station fallbacks are handled locally
185
    """
186
    __tablename__ = 'timeslot'
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
187

188
189
190
    # Primary keys
    id = Column(Integer, primary_key=True, autoincrement=True)

David Trattnig's avatar
David Trattnig committed
191
192
193
194
195
    # Relationships
    playlist = relationship("Playlist",
                            primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \
                                Timeslot.playlist_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)",
                            uselist=False, back_populates="timeslot")
196
197
198
199
200
201
202
    default_schedule_playlist = relationship("Playlist",
                            primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \
                                Timeslot.default_schedule_playlist_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)",
                            uselist=False, back_populates="timeslot")
    default_show_playlist = relationship("Playlist",
                            primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \
                                Timeslot.default_show_playlist_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)",
David Trattnig's avatar
David Trattnig committed
203
                            uselist=False, back_populates="timeslot")
David Trattnig's avatar
David Trattnig committed
204
205
206
    schedule_fallback = relationship("Playlist",
                            primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \
                                Timeslot.schedule_fallback_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)",
David Trattnig's avatar
David Trattnig committed
207
                            uselist=False, back_populates="timeslot")
David Trattnig's avatar
David Trattnig committed
208
209
210
211
212
213
214
215
216
217
    show_fallback = relationship("Playlist",
                            primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \
                                Timeslot.show_fallback_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)",
                            uselist=False, back_populates="timeslot")
    station_fallback = relationship("Playlist",
                            primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \
                                Timeslot.station_fallback_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)",
                            uselist=False, back_populates="timeslot")

    playlist_id = Column(Integer)
218
219
    default_schedule_playlist_id = Column(Integer)
    default_show_playlist_id = Column(Integer)
David Trattnig's avatar
David Trattnig committed
220
221
222
223
224
    schedule_fallback_id = Column(Integer)
    show_fallback_id = Column(Integer)
    station_fallback_id = Column(Integer)

    # Data
225
226
227
    timeslot_start = Column(DateTime, unique=True, index=True)
    timeslot_end = Column(DateTime, unique=True, index=True)
    timeslot_id = Column(Integer, unique=True)
228

229
    show_id = Column(Integer)
230
231
    show_name = Column(String(256))
    show_hosts = Column(String(256))
232
    funding_category = Column(String(256))
233
234
235
236
237
238
    comment = Column(String(512))
    languages = Column(String(256))
    type = Column(String(256))
    category = Column(String(256))
    topic = Column(String(256))
    musicfocus = Column(String(256))
239
    is_repetition = Column(Boolean())
David Trattnig's avatar
David Trattnig committed
240

241
242
    # Transients
    active_entry = None
243

244

245
    @staticmethod
246
247
248
249
250
251
252
    def for_datetime(date_time):
        """
        Select a timeslot at the given datetime.

        Args:
            date_time (datetime): date and time when the timeslot starts
        """
253
254
255
256
257
258
        with DB.Session() as session:
            return (
                session.query(Timeslot)
                .filter(Timeslot.timeslot_start == date_time)
                .first()
            )
259

260

261
    @staticmethod
262
    def get_timeslots(date_from=datetime.date.today()):
263
        """
264
        Select all timeslots starting from `date_from` or from today if no
265
266
267
        parameter is passed.

        Args:
268
            date_from (datetime):   Select timeslots from this date and time on
269
270

        Returns:
271
            ([Timeslot]):           List of timeslots
272
        """
273
274
275
276
277
278
279
        with DB.Session() as session:
            return (
                session.query(Timeslot)
                .filter(Timeslot.timeslot_start >= date_from)
                .order_by(Timeslot.timeslot_start)
                .all()
            )
280

281

282
    def set_active_entry(self, entry):
David Trattnig's avatar
David Trattnig committed
283
        """
284
285
286
287
        Sets the currently playing entry.

        Args:
            entry (PlaylistEntry): The entry playing right now
David Trattnig's avatar
David Trattnig committed
288
        """
289
        self.active_entry = entry
David Trattnig's avatar
David Trattnig committed
290
291


292
    def get_recent_entry(self):
David Trattnig's avatar
David Trattnig committed
293
        """
294
295
        Retrieves the most recent played or currently playing entry. This is used to fade-out
        the timeslot, when there is no other entry is following the current one.
David Trattnig's avatar
David Trattnig committed
296
        """
297
        return self.active_entry
David Trattnig's avatar
David Trattnig committed
298
299


David Trattnig's avatar
David Trattnig committed
300
301
302
    @hybrid_property
    def start_unix(self):
        """
303
        Start time of the timeslot in UNIX time.
David Trattnig's avatar
David Trattnig committed
304
        """
305
        return time.mktime(self.timeslot_start.timetuple())
David Trattnig's avatar
David Trattnig committed
306
307
308
309
310


    @hybrid_property
    def end_unix(self):
        """
311
        End time of the timeslot in UNIX time.
David Trattnig's avatar
David Trattnig committed
312
        """
313
        return time.mktime(self.timeslot_end.timetuple())
David Trattnig's avatar
David Trattnig committed
314
315


David Trattnig's avatar
David Trattnig committed
316
317
    def as_dict(self):
        """
318
        Returns the timeslot as a dictionary for serialization.
David Trattnig's avatar
David Trattnig committed
319
320
321
322
        """
        playlist = self.playlist

        return {
David Trattnig's avatar
David Trattnig committed
323
            "timeslot_id": self.timeslot_id,
324
325
            "timeslot_start": self.timeslot_start.isoformat(),
            "timeslot_end": self.timeslot_end.isoformat(),
David Trattnig's avatar
David Trattnig committed
326
327
328
329
330
331
332
333
334

            "topic": self.topic,
            "musicfocus": self.musicfocus,
            "funding_category": self.funding_category,
            "is_repetition": self.is_repetition,
            "category": self.category,
            "languages": self.languages,
            "comment": self.comment,
            "playlist_id": self.playlist_id,
335
            "schedule_default_id": self.schedule_default_id,
David Trattnig's avatar
David Trattnig committed
336
            "show_default_id": self.show_default_id,
David Trattnig's avatar
David Trattnig committed
337
338
339
340
341
342
343
344
345
346
347
348
349
            "schedule_fallback_id": self.schedule_fallback_id,
            "show_fallback_id": self.show_fallback_id,
            "station_fallback_id": self.station_fallback_id,

            "show": {
                "name": self.show_name,
                "host": self.show_hosts
            },

            "playlist": playlist
        }


350
351
352
353
    def __str__(self):
        """
        String representation of the object.
        """
David Trattnig's avatar
David Trattnig committed
354
355
        time_start = SimpleUtil.fmt_time(self.start_unix)
        time_end = SimpleUtil.fmt_time(self.end_unix)
356
        return "ID#%s [Show: %s, ShowID: %s | %s - %s ]" % (str(self.timeslot_id), self.show_name, str(self.show_id), time_start, time_end)
David Trattnig's avatar
David Trattnig committed
357
358


David Trattnig's avatar
David Trattnig committed
359
360
361
#
#   PLAYLIST
#
362

363
class Playlist(DB.Model, AuraDatabaseModel):
364
365
366
    """
    The playlist containing playlist entries.
    """
367
368
    __tablename__ = 'playlist'

369
370
371
372
373
    # Static Playlist Types
    TYPE_TIMESLOT = { "id": 0, "name": "timeslot" }
    TYPE_SCHEDULE = { "id": 1, "name": "schedule" }
    TYPE_SHOW = { "id": 2, "name": "show" }

David Trattnig's avatar
David Trattnig committed
374
    # Primary and Foreign Key
375
    artificial_id = Column(Integer, primary_key=True)
376
    timeslot_start = Column(DateTime, ForeignKey("timeslot.timeslot_start"))
David Trattnig's avatar
David Trattnig committed
377
378

    # Relationships
379
    timeslot = relationship("Timeslot", uselist=False, back_populates="playlist")
380
    entries = relationship("PlaylistEntry", back_populates="playlist")
David Trattnig's avatar
David Trattnig committed
381
382
383

    # Data
    playlist_id = Column(Integer, autoincrement=False)
384
    show_name = Column(String(256))
385
    entry_count = Column(Integer)
386

387

388
389
390
391
392
    # @staticmethod
    # def select_all():
    #     """
    #     Fetches all entries
    #     """
393
394
    #     with DB.Session() as session:
    #         all_entries = session.query(Playlist).filter(Playlist.fallback_type == 0).all()
395

396
397
398
399
    #     cnt = 0
    #     for entry in all_entries:
    #         entry.programme_index = cnt
    #         cnt = cnt + 1
400

401
    #     return all_entries
402

403

404
    @staticmethod
405
    def select_playlist_for_timeslot(start_date, playlist_id):
406
        """
407
        Retrieves the playlist for the given timeslot identified by `start_date` and `playlist_id`
408
409
410
411
412
413

        Args:
            start_date (datetime):  Date and time when the playlist is scheduled
            playlist_id (Integer):  The ID of the playlist

        Returns:
414
            (Playlist):             The playlist, if existing for timeslot
415
416
417
418

        Raises:
            Exception:              In case there a inconsistent database state, such es multiple playlists for given date/time.
        """
David Trattnig's avatar
David Trattnig committed
419
        playlist = None
420
421
422
423
424
425
        with DB.Session() as session:
            playlists = (
                session.query(Playlist)
                .filter(Playlist.timeslot_start == start_date)
                .all()
            )
426

David Trattnig's avatar
David Trattnig committed
427
428
429
        for p in playlists:
            if p.playlist_id == playlist_id:
                playlist = p
430

David Trattnig's avatar
David Trattnig committed
431
        return playlist
432

433

434
435
    @staticmethod
    def select_playlist(playlist_id):
436
437
438
439
440
        """
        Retrieves all paylists for that given playlist ID.

        Args:
            playlist_id (Integer):  The ID of the playlist
David Trattnig's avatar
David Trattnig committed
441

442
443
444
        Returns:
            (Array<Playlist>):      An array holding the playlists
        """
445
446
447
448
449
450
451
        with DB.Session() as session:
            return (
                session.query(Playlist)
                .filter(Playlist.playlist_id == playlist_id)
                .order_by(Playlist.timeslot_start)
                .all()
            )
David Trattnig's avatar
David Trattnig committed
452

453

454
455
456
457
458
    @staticmethod
    def is_empty():
        """
        Checks if the given is empty
        """
459
460
461
462
463
        with DB.Session() as session:
            try:
                return not session.query(Playlist).one_or_none()
            except sa.orm.exc.MultipleResultsFound:
                return False
464
465


466
467
    @hybrid_property
    def start_unix(self):
468
469
470
        """
        Start time of the playlist in UNIX time.
        """
471
        return time.mktime(self.timeslot_start.timetuple())
472

473

474
475
476
477
478
    @hybrid_property
    def end_unix(self):
        """
        End time of the playlist in UNIX time.
        """
479
        return time.mktime(self.timeslot_start.timetuple()) + self.duration
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496


    @hybrid_property
    def duration(self):
        """
        Returns the total length of the playlist in seconds.

        Returns:
            (Integer):  Length in seconds
        """
        total = 0

        for entry in self.entries:
            total += entry.duration
        return total


497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
    # def as_dict(self):
    #     """
    #     Returns the playlist as a dictionary for serialization.
    #     """
    #     entries = []
    #     for e in self.entries:
    #         entries.append(e.as_dict())

    #     playlist = {
    #         "playlist_id": self.playlist_id,
    #         "fallback_type": self.fallback_type,
    #         "entry_count": self.entry_count,
    #         "entries": entries
    #     }
    #     return playlist
David Trattnig's avatar
David Trattnig committed
512
513


514
515
516
517
    def __str__(self):
        """
        String representation of the object.
        """
David Trattnig's avatar
David Trattnig committed
518
519
520
521
        time_start = SimpleUtil.fmt_time(self.start_unix)
        time_end = SimpleUtil.fmt_time(self.end_unix)
        return "ID#%s [items: %s | %s - %s]" % (str(self.playlist_id), str(self.entry_count), str(time_start), str(time_end))

522

David Trattnig's avatar
David Trattnig committed
523
524
525
#
#   PLAYLIST ENTRY
#
526

527
class PlaylistEntry(DB.Model, AuraDatabaseModel):
528
529
530
    """
    Playlist entries are the individual items of a playlist such as audio files.
    """
531
    __tablename__ = 'playlist_entry'
532

David Trattnig's avatar
David Trattnig committed
533
    # Primary and Foreign Keys
534
535
    artificial_id = Column(Integer, primary_key=True)
    artificial_playlist_id = Column(Integer, ForeignKey("playlist.artificial_id"))
536

David Trattnig's avatar
David Trattnig committed
537
538
539
540
541
542
    # Relationships
    playlist = relationship("Playlist", uselist=False, back_populates="entries")
    meta_data = relationship("PlaylistEntryMetaData", uselist=False, back_populates="entry")

    # Data
    entry_num = Column(Integer)
543
    duration = Column(BigInteger)
David Trattnig's avatar
David Trattnig committed
544
    volume = Column(Integer, ColumnDefault(100))
545
    source = Column(String(1024))
546
    entry_start = Column(DateTime)
David Trattnig's avatar
David Trattnig committed
547

548
    # Transients
549
    entry_start_actual = None # Assigned when the entry is actually played
550
    channel = None # Assigned when entry is actually played
David Trattnig's avatar
David Trattnig committed
551
    queue_state = None # Assigned when entry is about to be queued
David Trattnig's avatar
David Trattnig committed
552
    status = None # Assigned when state changes
553

554

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
555
    @staticmethod
556
    def select_playlistentry_for_playlist(artificial_playlist_id, entry_num):
557
558
559
        """
        Selects one entry identified by `playlist_id` and `entry_num`.
        """
560
561
562
563
564
565
566
        with DB.Session() as session:
            return (
                session.query(PlaylistEntry)
                .filter(PlaylistEntry.entry_num == entry_num)
                .filter(PlaylistEntry.artificial_playlist_id == artificial_playlist_id)
                .first()
            )
567

568
569
570
571
572
    @staticmethod
    def delete_entry(artificial_playlist_id, entry_num):
        """
        Deletes the playlist entry and associated metadata.
        """
573
574
575
576
577
578
        with DB.Session() as session:
            entry = PlaylistEntry.select_playlistentry_for_playlist(artificial_playlist_id, entry_num)
            metadata = PlaylistEntryMetaData.select_metadata_for_entry(entry.artificial_id)
            metadata.delete()
            entry.delete()
            session.commit()
579
580
581
582
583
584

    @staticmethod
    def count_entries(artificial_playlist_id):
        """
        Returns the count of all entries.
        """
585
586
587
588
589
590
        with DB.Session() as session:
            return (
                session.query(PlaylistEntry)
                .filter(PlaylistEntry.artificial_playlist_id == artificial_playlist_id)
                .count()
            )
591

592
593
594
595
    @hybrid_property
    def entry_end(self):
        return self.entry_start + datetime.timedelta(seconds=self.duration)

596
597
598
599
600
601
    @hybrid_property
    def start_unix(self):
        return time.mktime(self.entry_start.timetuple())

    @hybrid_property
    def end_unix(self):
602
        return time.mktime(self.entry_end.timetuple())
603

604

David Trattnig's avatar
David Trattnig committed
605
    def get_content_type(self):
606
        return ResourceUtil.get_content_type(self.source)
607

608

609
610
611
612
613
614
615
616
617
618
619
620
621
622
    def get_prev_entries(self):
        """
        Retrieves all previous entries as part of the current entry's playlist.

        Returns:
            (List):     List of PlaylistEntry
        """
        prev_entries = []
        for entry in self.playlist.entries:
            if entry.entry_start < self.entry_start:
                prev_entries.append(entry)
        return prev_entries


623
    def get_next_entries(self, timeslot_sensitive=True):
David Trattnig's avatar
David Trattnig committed
624
        """
625
        Retrieves all following entries as part of the current entry's playlist.
David Trattnig's avatar
David Trattnig committed
626

627
        Args:
628
629
            timeslot_sensitive (Boolean):   If `True` entries which start after \
                the end of the timeslot are excluded
630

David Trattnig's avatar
David Trattnig committed
631
632
633
634
635
636
        Returns:
            (List):     List of PlaylistEntry
        """
        next_entries = []
        for entry in self.playlist.entries:
            if entry.entry_start > self.entry_start:
637
638
                if timeslot_sensitive:
                    if entry.entry_start < self.playlist.timeslot.timeslot_end:
639
640
641
                        next_entries.append(entry)
                else:
                    next_entries.append(entry)
David Trattnig's avatar
David Trattnig committed
642
643
        return next_entries

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
644

645
646
647
648
649
650
    def as_dict(self):
        """
        Returns the entry as a dictionary for serialization.
        """
        if self.meta_data:
            return {
David Trattnig's avatar
David Trattnig committed
651
                "id": self.artificial_id,
652
653
654
655
656
657
658
                "duration": self.duration,
                "artist": self.meta_data.artist,
                "album": self.meta_data.album,
                "title": self.meta_data.title
            }
        return None

659
660
661
662
    def __str__(self):
        """
        String representation of the object.
        """
David Trattnig's avatar
David Trattnig committed
663
664
        time_start = SimpleUtil.fmt_time(self.start_unix)
        time_end = SimpleUtil.fmt_time(self.end_unix)
665
666
        track = self.source[-25:]
        return "PlaylistEntry #%s [%s - %s | %ssec | Source: ...%s]" % (str(self.artificial_id), time_start, time_end, self.duration, track)
667

David Trattnig's avatar
David Trattnig committed
668

669

David Trattnig's avatar
David Trattnig committed
670
671
672
673
#
#   PLAYLIST ENTRY METADATA
#

674

675
class PlaylistEntryMetaData(DB.Model, AuraDatabaseModel):
676
    """
David Trattnig's avatar
David Trattnig committed
677
    Metadata for a playlist entry such as the artist, album and track name.
678
    """
679
    __tablename__ = "playlist_entry_metadata"
680

David Trattnig's avatar
David Trattnig committed
681
    # Primary and Foreign Keys
682
683
    artificial_id = Column(Integer, primary_key=True)
    artificial_entry_id = Column(Integer, ForeignKey("playlist_entry.artificial_id"))
684

David Trattnig's avatar
David Trattnig committed
685
686
687
688
    # Relationships
    entry = relationship("PlaylistEntry", uselist=False, back_populates="meta_data")

    # Data
689
690
691
    artist = Column(String(256))
    title = Column(String(256))
    album = Column(String(256))
692

693
694
    @staticmethod
    def select_metadata_for_entry(artificial_playlistentry_id):
695
696
697
698
699
700
        with DB.Session() as session:
            return (
                session.query(PlaylistEntryMetaData)
                .filter(PlaylistEntryMetaData.artificial_entry_id == artificial_playlistentry_id)
                .first()
            )