models.py 19.8 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 sys
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
21
import time
22
23
import logging
import datetime
24

25
26
import sqlalchemy as sa

David Trattnig's avatar
David Trattnig committed
27
28
29
30
31
32
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
33

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

David Trattnig's avatar
David Trattnig committed
38
# Initialize DB Model and session
David Trattnig's avatar
David Trattnig committed
39
config = AuraConfig()
David Trattnig's avatar
David Trattnig committed
40
engine = sa.create_engine(config.get_database_uri())
41
42
43
44
Base = declarative_base()
Base.metadata.bind = engine

class DB():
David Trattnig's avatar
David Trattnig committed
45
46
47
    session_factory = sessionmaker(bind=engine)
    Session = scoped_session(session_factory)
    session = Session()
48
49
50
    Model = Base


51
52

class AuraDatabaseModel():
53
54
55
56
57
    """
    AuraDataBaseModel.

    Holding all tables and relationships for the engine.
    """
58
59
    logger = None

60

61
    def __init__(self):
62
63
64
        """
        Constructor.
        """
65
66
        self.logger = logging.getLogger("AuraEngine")

67

68
    def store(self, add=False, commit=False):
69
70
71
        """
        Store to the database
        """
72
        if add:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
73
            DB.session.add(self)
74
75
        else:
            DB.session.merge(self)
76
        if commit:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
77
            DB.session.commit()
78

79

80
    def delete(self, commit=False):
81
82
83
        """
        Delete from the database
        """
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
84
        DB.session.delete(self)
85
        if commit:
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
86
            DB.session.commit()
87

88

89
90
91
92
93
94
95
96
    def refresh(self):
        """
        Refreshes the currect record
        """
        DB.session.expire(self)
        DB.session.refresh(self)


97
98
    def _asdict(self):
        return self.__dict__
99

100

101
102
103
    @staticmethod
    def init_database():
        """
104
        Initializes the database tables if they are not existing.
105
106
107
108
109
110
111

        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
112
            is_available = True
113

David Trattnig's avatar
David Trattnig committed
114
115
116
117
118
119
120
121
            # 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:
122
123
124
125
126
127
                model = AuraDatabaseModel()
                model.recreate_db()
            else:
                raise


128
129
    @staticmethod
    def recreate_db(systemexit = False):
130
        """
131
        Deletes all tables and re-creates the database.
David Trattnig's avatar
David Trattnig committed
132
        """
133
134
        Base.metadata.drop_all()
        Base.metadata.create_all()
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
135
        DB.session.commit()
136

137
138
        if systemexit:
            sys.exit(0)
139

140

141
142

#
David Trattnig's avatar
David Trattnig committed
143
#   TIMESLOT
144
145
#

146

147
class Timeslot(DB.Model, AuraDatabaseModel):
148
    """
David Trattnig's avatar
David Trattnig committed
149
    One specific timeslot for a show.
150
151
152
153
154
155
156
157

    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
158
    """
159
    __tablename__ = 'timeslot'
Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
160

161
162
163
    # Primary keys
    id = Column(Integer, primary_key=True, autoincrement=True)

David Trattnig's avatar
David Trattnig committed
164
165
166
167
168
    # 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")
169
170
171
172
173
174
175
    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
176
                            uselist=False, back_populates="timeslot")
David Trattnig's avatar
David Trattnig committed
177
178
179
    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
180
                            uselist=False, back_populates="timeslot")
David Trattnig's avatar
David Trattnig committed
181
182
183
184
185
186
187
188
189
190
    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)
191
192
    default_schedule_playlist_id = Column(Integer)
    default_show_playlist_id = Column(Integer)
David Trattnig's avatar
David Trattnig committed
193
194
195
196
197
    schedule_fallback_id = Column(Integer)
    show_fallback_id = Column(Integer)
    station_fallback_id = Column(Integer)

    # Data
198
199
200
    timeslot_start = Column(DateTime, unique=True, index=True)
    timeslot_end = Column(DateTime, unique=True, index=True)
    timeslot_id = Column(Integer, unique=True)
201

202
    show_id = Column(Integer)
203
204
    show_name = Column(String(256))
    show_hosts = Column(String(256))
205
    funding_category = Column(String(256))
206
207
208
209
210
211
    comment = Column(String(512))
    languages = Column(String(256))
    type = Column(String(256))
    category = Column(String(256))
    topic = Column(String(256))
    musicfocus = Column(String(256))
212
    is_repetition = Column(Boolean())
David Trattnig's avatar
David Trattnig committed
213

214
215
    # Transients
    active_entry = None
216

217

218
    @staticmethod
219
220
221
222
223
224
225
    def for_datetime(date_time):
        """
        Select a timeslot at the given datetime.

        Args:
            date_time (datetime): date and time when the timeslot starts
        """
226
        return DB.session.query(Timeslot).filter(Timeslot.timeslot_start == date_time).first()
227

228

229
    @staticmethod
230
    def get_timeslots(date_from=datetime.date.today()):
231
        """
232
        Select all timeslots starting from `date_from` or from today if no
233
234
235
        parameter is passed.

        Args:
236
            date_from (datetime):   Select timeslots from this date and time on
237
238

        Returns:
239
            ([Timeslot]):           List of timeslots
240
        """
241
242
        timeslots = DB.session.query(Timeslot).\
            filter(Timeslot.timeslot_start >= date_from).\
David Trattnig's avatar
David Trattnig committed
243
            order_by(Timeslot.timeslot_start).all()
244
        return timeslots
245

246

247
    def set_active_entry(self, entry):
David Trattnig's avatar
David Trattnig committed
248
        """
249
250
251
252
        Sets the currently playing entry.

        Args:
            entry (PlaylistEntry): The entry playing right now
David Trattnig's avatar
David Trattnig committed
253
        """
254
        self.active_entry = entry
David Trattnig's avatar
David Trattnig committed
255
256


257
    def get_recent_entry(self):
David Trattnig's avatar
David Trattnig committed
258
        """
259
260
        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
261
        """
262
        return self.active_entry
David Trattnig's avatar
David Trattnig committed
263
264


David Trattnig's avatar
David Trattnig committed
265
266
267
    @hybrid_property
    def start_unix(self):
        """
268
        Start time of the timeslot in UNIX time.
David Trattnig's avatar
David Trattnig committed
269
        """
270
        return time.mktime(self.timeslot_start.timetuple())
David Trattnig's avatar
David Trattnig committed
271
272
273
274
275


    @hybrid_property
    def end_unix(self):
        """
276
        End time of the timeslot in UNIX time.
David Trattnig's avatar
David Trattnig committed
277
        """
278
        return time.mktime(self.timeslot_end.timetuple())
David Trattnig's avatar
David Trattnig committed
279
280


David Trattnig's avatar
David Trattnig committed
281
282
    def as_dict(self):
        """
283
        Returns the timeslot as a dictionary for serialization.
David Trattnig's avatar
David Trattnig committed
284
285
286
287
        """
        playlist = self.playlist

        return {
David Trattnig's avatar
David Trattnig committed
288
            "timeslot_id": self.timeslot_id,
289
290
            "timeslot_start": self.timeslot_start.isoformat(),
            "timeslot_end": self.timeslot_end.isoformat(),
David Trattnig's avatar
David Trattnig committed
291
292
293
294
295
296
297
298
299

            "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,
300
            "schedule_default_id": self.schedule_default_id,
David Trattnig's avatar
David Trattnig committed
301
            "show_default_id": self.show_default_id,
David Trattnig's avatar
David Trattnig committed
302
303
304
305
306
307
308
309
310
311
312
313
314
            "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
        }


315
316
317
318
    def __str__(self):
        """
        String representation of the object.
        """
David Trattnig's avatar
David Trattnig committed
319
320
        time_start = SimpleUtil.fmt_time(self.start_unix)
        time_end = SimpleUtil.fmt_time(self.end_unix)
321
        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
322
323


David Trattnig's avatar
David Trattnig committed
324
325
326
#
#   PLAYLIST
#
327

328
class Playlist(DB.Model, AuraDatabaseModel):
329
330
331
    """
    The playlist containing playlist entries.
    """
332
333
    __tablename__ = 'playlist'

334
335
336
337
338
    # 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
339
    # Primary and Foreign Key
340
    artificial_id = Column(Integer, primary_key=True)
341
    timeslot_start = Column(DateTime, ForeignKey("timeslot.timeslot_start"))
David Trattnig's avatar
David Trattnig committed
342
343

    # Relationships
344
    timeslot = relationship("Timeslot", uselist=False, back_populates="playlist")
345
    entries = relationship("PlaylistEntry", back_populates="playlist")
David Trattnig's avatar
David Trattnig committed
346
347
348

    # Data
    playlist_id = Column(Integer, autoincrement=False)
349
    show_name = Column(String(256))
350
    entry_count = Column(Integer)
351

352

353
354
355
356
357
358
    # @staticmethod
    # def select_all():
    #     """
    #     Fetches all entries
    #     """
    #     all_entries = DB.session.query(Playlist).filter(Playlist.fallback_type == 0).all()
359

360
361
362
363
    #     cnt = 0
    #     for entry in all_entries:
    #         entry.programme_index = cnt
    #         cnt = cnt + 1
364

365
    #     return all_entries
366

367

368
    @staticmethod
369
    def select_playlist_for_timeslot(start_date, playlist_id):
370
        """
371
        Retrieves the playlist for the given timeslot identified by `start_date` and `playlist_id`
372
373
374
375
376
377

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

        Returns:
378
            (Playlist):             The playlist, if existing for timeslot
379
380
381
382

        Raises:
            Exception:              In case there a inconsistent database state, such es multiple playlists for given date/time.
        """
David Trattnig's avatar
David Trattnig committed
383
        playlist = None
384
        playlists = DB.session.query(Playlist).filter(Playlist.timeslot_start == start_date).all()
385

David Trattnig's avatar
David Trattnig committed
386
387
388
        for p in playlists:
            if p.playlist_id == playlist_id:
                playlist = p
389

David Trattnig's avatar
David Trattnig committed
390
        return playlist
391

392

393
394
    @staticmethod
    def select_playlist(playlist_id):
395
396
397
398
399
        """
        Retrieves all paylists for that given playlist ID.

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

401
402
403
        Returns:
            (Array<Playlist>):      An array holding the playlists
        """
404
        return DB.session.query(Playlist).filter(Playlist.playlist_id == playlist_id).order_by(Playlist.timeslot_start).all()
David Trattnig's avatar
David Trattnig committed
405

406

407
408
409
410
411
412
413
414
415
416
417
    @staticmethod
    def is_empty():
        """
        Checks if the given is empty
        """
        try:
            return not DB.session.query(Playlist).one_or_none()
        except sa.orm.exc.MultipleResultsFound:
            return False


418
419
    @hybrid_property
    def start_unix(self):
420
421
422
        """
        Start time of the playlist in UNIX time.
        """
423
        return time.mktime(self.timeslot_start.timetuple())
424

425

426
427
428
429
430
    @hybrid_property
    def end_unix(self):
        """
        End time of the playlist in UNIX time.
        """
431
        return time.mktime(self.timeslot_start.timetuple()) + self.duration
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448


    @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


449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    # 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
464
465


466
467
468
469
    def __str__(self):
        """
        String representation of the object.
        """
David Trattnig's avatar
David Trattnig committed
470
471
472
473
        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))

474

David Trattnig's avatar
David Trattnig committed
475
476
477
#
#   PLAYLIST ENTRY
#
478

479
class PlaylistEntry(DB.Model, AuraDatabaseModel):
480
481
482
    """
    Playlist entries are the individual items of a playlist such as audio files.
    """
483
    __tablename__ = 'playlist_entry'
484

David Trattnig's avatar
David Trattnig committed
485
    # Primary and Foreign Keys
486
487
    artificial_id = Column(Integer, primary_key=True)
    artificial_playlist_id = Column(Integer, ForeignKey("playlist.artificial_id"))
488

David Trattnig's avatar
David Trattnig committed
489
490
491
492
493
494
    # Relationships
    playlist = relationship("Playlist", uselist=False, back_populates="entries")
    meta_data = relationship("PlaylistEntryMetaData", uselist=False, back_populates="entry")

    # Data
    entry_num = Column(Integer)
495
    duration = Column(BigInteger)
David Trattnig's avatar
David Trattnig committed
496
    volume = Column(Integer, ColumnDefault(100))
497
    source = Column(String(1024))
498
    entry_start = Column(DateTime)
David Trattnig's avatar
David Trattnig committed
499

500
    # Transients
501
    entry_start_actual = None # Assigned when the entry is actually played
502
    channel = None # Assigned when entry is actually played
David Trattnig's avatar
David Trattnig committed
503
    queue_state = None # Assigned when entry is about to be queued
David Trattnig's avatar
David Trattnig committed
504
    status = None # Assigned when state changes
505

506

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
507
    @staticmethod
508
    def select_playlistentry_for_playlist(artificial_playlist_id, entry_num):
509
510
511
        """
        Selects one entry identified by `playlist_id` and `entry_num`.
        """
512
        return DB.session.query(PlaylistEntry).filter(PlaylistEntry.artificial_playlist_id == artificial_playlist_id, PlaylistEntry.entry_num == entry_num).first()
513

514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
    @staticmethod
    def delete_entry(artificial_playlist_id, entry_num):
        """
        Deletes the playlist entry and associated metadata.
        """
        entry = PlaylistEntry.select_playlistentry_for_playlist(artificial_playlist_id, entry_num)
        metadata = PlaylistEntryMetaData.select_metadata_for_entry(entry.artificial_id)
        metadata.delete()
        entry.delete()
        DB.session.commit()

    @staticmethod
    def count_entries(artificial_playlist_id):
        """
        Returns the count of all entries.
        """
        result = DB.session.query(PlaylistEntry).filter(PlaylistEntry.artificial_playlist_id == artificial_playlist_id).count()
        return result

533
534
535
536
    @hybrid_property
    def entry_end(self):
        return self.entry_start + datetime.timedelta(seconds=self.duration)

537
538
539
540
541
542
    @hybrid_property
    def start_unix(self):
        return time.mktime(self.entry_start.timetuple())

    @hybrid_property
    def end_unix(self):
543
        return time.mktime(self.entry_end.timetuple())
544

545

David Trattnig's avatar
David Trattnig committed
546
    def get_content_type(self):
547
        return ResourceUtil.get_content_type(self.source)
548

549

550
551
552
553
554
555
556
557
558
559
560
561
562
563
    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


564
    def get_next_entries(self, timeslot_sensitive=True):
David Trattnig's avatar
David Trattnig committed
565
        """
566
        Retrieves all following entries as part of the current entry's playlist.
David Trattnig's avatar
David Trattnig committed
567

568
        Args:
569
570
            timeslot_sensitive (Boolean):   If `True` entries which start after \
                the end of the timeslot are excluded
571

David Trattnig's avatar
David Trattnig committed
572
573
574
575
576
577
        Returns:
            (List):     List of PlaylistEntry
        """
        next_entries = []
        for entry in self.playlist.entries:
            if entry.entry_start > self.entry_start:
578
579
                if timeslot_sensitive:
                    if entry.entry_start < self.playlist.timeslot.timeslot_end:
580
581
582
                        next_entries.append(entry)
                else:
                    next_entries.append(entry)
David Trattnig's avatar
David Trattnig committed
583
584
        return next_entries

Gottfried Gaisbauer's avatar
Gottfried Gaisbauer committed
585

586
587
588
589
590
591
    def as_dict(self):
        """
        Returns the entry as a dictionary for serialization.
        """
        if self.meta_data:
            return {
David Trattnig's avatar
David Trattnig committed
592
                "id": self.artificial_id,
593
594
595
596
597
598
599
                "duration": self.duration,
                "artist": self.meta_data.artist,
                "album": self.meta_data.album,
                "title": self.meta_data.title
            }
        return None

600
601
602
603
    def __str__(self):
        """
        String representation of the object.
        """
David Trattnig's avatar
David Trattnig committed
604
605
        time_start = SimpleUtil.fmt_time(self.start_unix)
        time_end = SimpleUtil.fmt_time(self.end_unix)
606
607
        track = self.source[-25:]
        return "PlaylistEntry #%s [%s - %s | %ssec | Source: ...%s]" % (str(self.artificial_id), time_start, time_end, self.duration, track)
608

David Trattnig's avatar
David Trattnig committed
609

610

David Trattnig's avatar
David Trattnig committed
611
612
613
614
#
#   PLAYLIST ENTRY METADATA
#

615

616
class PlaylistEntryMetaData(DB.Model, AuraDatabaseModel):
617
    """
David Trattnig's avatar
David Trattnig committed
618
    Metadata for a playlist entry such as the artist, album and track name.
619
    """
620
    __tablename__ = "playlist_entry_metadata"
621

David Trattnig's avatar
David Trattnig committed
622
    # Primary and Foreign Keys
623
624
    artificial_id = Column(Integer, primary_key=True)
    artificial_entry_id = Column(Integer, ForeignKey("playlist_entry.artificial_id"))
625

David Trattnig's avatar
David Trattnig committed
626
627
628
629
    # Relationships
    entry = relationship("PlaylistEntry", uselist=False, back_populates="meta_data")

    # Data
630
631
632
    artist = Column(String(256))
    title = Column(String(256))
    album = Column(String(256))
633

634
635
    @staticmethod
    def select_metadata_for_entry(artificial_playlistentry_id):
636
        return DB.session.query(PlaylistEntryMetaData).filter(PlaylistEntryMetaData.artificial_entry_id == artificial_playlistentry_id).first()
637
638