From 9046b6612edd319eaa9187eabfb09e4aa2639c9b Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Tue, 8 Sep 2020 18:40:47 +0200
Subject: [PATCH] Update/delete playlist entries. #31

---
 modules/base/models.py          | 680 ++------------------------------
 modules/plugins/trackservice.py |   2 +-
 modules/scheduling/calendar.py  |  49 ++-
 3 files changed, 67 insertions(+), 664 deletions(-)

diff --git a/modules/base/models.py b/modules/base/models.py
index 528c7a4d..e062cf85 100644
--- a/modules/base/models.py
+++ b/modules/base/models.py
@@ -88,6 +88,14 @@ class AuraDatabaseModel():
             DB.session.commit()
 
 
+    def refresh(self):
+        """
+        Refreshes the currect record
+        """
+        DB.session.expire(self)
+        DB.session.refresh(self)
+
+
     def _asdict(self):
         return self.__dict__
 
@@ -301,7 +309,7 @@ class Playlist(DB.Model, AuraDatabaseModel):
 
 
     @staticmethod
-    def select_playlist_for_schedule(datetime, playlist_id):
+    def select_playlist_for_schedule(start_date, playlist_id):
         """
         Retrieves the playlist for the given schedule identified by `start_date` and `playlist_id`
 
@@ -316,18 +324,14 @@ class Playlist(DB.Model, AuraDatabaseModel):
             Exception:              In case there a inconsistent database state, such es multiple playlists for given date/time.
         """
         playlist = None
-        playlists = DB.session.query(Playlist).filter(Playlist.schedule_start == datetime).all()
+        playlists = DB.session.query(Playlist).filter(Playlist.schedule_start == start_date).all()
         # FIXME There are unknown issues with the native SQL query by ID
         # playlists = DB.session.query(Playlist).filter(Playlist.schedule_start == datetime and Playlist.playlist_id == playlist_id).all()
         
         for p in playlists:
             if p.playlist_id == playlist_id:
                 playlist = p
-        # if playlists and len(playlists) > 1:
-        #     raise Exception("Inconsistent Database State: Multiple playlists for given schedule '%s' and playlist id#%d available!" % (str(datetime), playlist_id))
-        # if not playlists:
-        #     return None
-        # return playlists[0]
+
         return playlist
 
 
@@ -446,8 +450,30 @@ class PlaylistEntry(DB.Model, AuraDatabaseModel):
 
     @staticmethod
     def select_playlistentry_for_playlist(artificial_playlist_id, entry_num):
+        """
+        Selects one entry identified by `playlist_id` and `entry_num`.
+        """
         return DB.session.query(PlaylistEntry).filter(PlaylistEntry.artificial_playlist_id == artificial_playlist_id, PlaylistEntry.entry_num == entry_num).first()
 
+    @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
+
     @hybrid_property
     def entry_end(self):
         return self.entry_start + datetime.timedelta(seconds=self.duration)
@@ -547,644 +573,6 @@ class PlaylistEntryMetaData(DB.Model, AuraDatabaseModel):
 
     @staticmethod
     def select_metadata_for_entry(artificial_playlistentry_id):
-        return DB.session.query(PlaylistEntry).filter(PlaylistEntryMetaData.artificial_entry_id == artificial_playlistentry_id).first()
-
-
-
-#
-#   TRACK SERVICE
-#
-
-
-# class TrackService(DB.Model, AuraDatabaseModel):
-#     """
-#     TrackService holding track-service items consisting of
-#     """
-#     __tablename__ = 'trackservice'
-
-#     # Primary keys
-#     id = Column(Integer, primary_key=True, autoincrement=True)
-
-#     # Foreign keys
-#     track_start = Column(DateTime)
-#     track_end = Column(DateTime) # Currently not used, maybe later for timing checks and multi-entry avoidance.
-#     artificial_schedule_id = Column(Integer, ForeignKey("schedule.id"))
-#     artificial_playlist_entry_id = Column(Integer, ForeignKey("playlist_entry.artificial_id"), nullable=True)
-#     single_entry_id = Column(Integer, ForeignKey("single_entry.id"), nullable=True)
-
-#     # Data
-#     schedule = relationship("Schedule", foreign_keys=[artificial_schedule_id], lazy="joined")
-#     playlist_entry = relationship("PlaylistEntry", primaryjoin="and_(TrackService.artificial_playlist_entry_id==PlaylistEntry.artificial_id)", lazy="joined")
-#     single_entry = relationship("SingleEntry", foreign_keys=[single_entry_id], lazy="joined")
-
-#     fallback_type = Column(Integer, default=0)
-
-
-#     def __init__(self, entry, fallback_type=0):
-#         """
-#         Initializes a trackservice entry based on a playlist entry.
-#         """
-#         self.track_start = datetime.datetime.now()
-#         # if entry.duration:
-#         #     self.track_end = self.track_start + datetime.timedelta(seconds=entry.duration)
-#         self.fallback_type = fallback_type
-
-#         if fallback_type < 4:
-#             self.schedule_start = entry.playlist.schedule_start
-#             self.artificial_playlist_entry_id = entry.artificial_id
-#             self.playlist_entry = entry
-#             self.schedule = entry.playlist.schedule
-#         else:
-#             self.single_entry = entry
-
-
-#     @hybrid_property
-#     def track(self):
-#         """
-#         Retrieves the track information as a dictionary.
-        
-#         Depending on possible fallback scenarios either `playlist_entry` or `single_entry` is used as a basis:
-
-#             - Scenario 1: No fallback, all info is gathered via the playlist entry
-#             - Scenario 2: Fallback-type > 0, info is also gathered via the defined playlist entry
-#             - Scenario 3: This type of fallback didn't get scheduled; a single entry is played
-#         """
-#         if self.playlist_entry:
-#             return self.playlist_entry.as_dict()
-#         elif self.single_entry:
-#             return self.single_entry.as_dict()
-#         else:
-#             return None
-
-
-#     @hybrid_property
-#     def show(self):
-#         """
-#         Retrieves show information based on the related schedule. If no schedule
-#         is available (e.g. when the engine is in a fallback state), then the default
-#         show properties from `AuraConfig` are returned.
-#         """
-#         show_info = {}
-
-#         if self.schedule:
-#             show_info["name"] = self.schedule.show_name
-#             show_info["type"] = self.schedule.type
-#             show_info["host"] = self.schedule.show_hosts
-#         elif self.fallback_type == 4:
-#             show_info["name"] = config.get("fallback_show_name")
-#             show_info["type"] = config.get("fallback_show_type")
-#             show_info["host"] = config.get("fallback_show_host")
-        
-#         return show_info
-
-
-#     @staticmethod
-#     def select_one(id):
-#         """
-#         Select one specific track-service item by ID. 
-#         """
-#         DB.session.commit() # Required since independend session is used.
-#         track = DB.session.query(TrackService).filter(TrackService.id == id).first()
-#         return track
-
-
-#     @staticmethod
-#     def select_current():
-#         """
-#         Selects the currently playing track.
-#         """
-#         now = datetime.datetime.now()
-#         DB.session.commit() # Required since independend session is used.
-#         track = DB.session.query(TrackService).\
-#             filter(TrackService.track_start <= str(now)).\
-#             order_by(TrackService.track_start.desc()).first()
-#         return track
-
-
-#     @staticmethod
-#     def select_last_hours(n):
-#         """
-#         Selects the tracks playing in the past (`n`) hours.
-#         """
-#         last_hours = datetime.datetime.today() - datetime.timedelta(hours=n)
-#         DB.session.commit() # Required since independend session is used.
-#         tracks = DB.session.query(TrackService).filter(TrackService.track_start >= str(last_hours)).all()
-#         for track in tracks:
-#             track = TrackService.select_one(track.id)
-#         return tracks
-
-
-#     @staticmethod
-#     def select_by_day(day):
-#         """
-#         Select the track-service items for a day.
-#         """
-#         day_plus_one = day + datetime.timedelta(days=1)
-#         DB.session.commit() # Required since independend session is used.
-#         tracks = DB.session.query(TrackService).\
-#             filter(TrackService.track_start >= str(day), TrackService.track_start < str(day_plus_one)).\
-#             order_by(TrackService.track_start.desc()).all()
-        
-#         res = []
-#         for item in tracks:
-#             if item.track: res.append(item)
-#         return res
-    
-
-#     @staticmethod
-#     def select_by_range(from_day, to_day):
-#         """
-#         Selects the track-service items for a day range.
-#         """
-#         DB.session.commit()
-#         tracks = DB.session.query(TrackService).filter(TrackService.track_start >= str(from_day),
-#                                                        TrackService.track_start < str(to_day)).all()
-#         return tracks
-    
-
-#     def __str__(self):
-#         """
-#         Convert to String.
-#         """
-#         return "TrackID: #%s [track_start: %s, artificial_playlist_entry_id: %s]" % (str(self.id), str(self.track_start), str(self.artificial_playlist_entry_id))
-
-
-
-# class SingleEntry(DB.Model, AuraDatabaseModel):
-#     """
-#     An entry played in case of e.g. a local fallback or custom programming without a playlist nor schedule.
-#     """
-#     __tablename__ = 'single_entry'
-
-#     # Primary keys
-#     id = Column(Integer, primary_key=True)
-    
-#     # Relationships
-#     trackservice_id = Column(Integer) #, ForeignKey("trackservice.id"))
-#     meta_data_id = Column(Integer) #, ForeignKey("trackservice.id"))
-
-#     trackservice = relationship("TrackService", uselist=False, back_populates="single_entry")
-#     meta_data = relationship("SingleEntryMetaData", uselist=False, back_populates="entry")
-
-#     # Data
-#     uri = Column(String(1024))
-#     duration = Column(BigInteger)
-#     source = Column(String(1024))
-#     entry_start = Column(DateTime)
-
-#     queue_state = None # Assigned when entry is about to be queued
-#     channel = None # Assigned when entry is actually played
-#     status = None # Assigned when state changes
-
-
-#     @hybrid_property
-#     def entry_end(self):
-#         return self.entry_start + datetime.timedelta(seconds=self.duration)
-
-#     @hybrid_property
-#     def start_unix(self):
-#         return time.mktime(self.entry_start.timetuple())
-
-#     @hybrid_property
-#     def end_unix(self):
-#         return time.mktime(self.entry_start.timetuple()) + self.duration
-
-#     @hybrid_property
-#     def volume(self):
-#         return 100
-
-#     @hybrid_property
-#     def type(self):
-#         return EngineUtil.get_channel_type(self.uri)
-
-
-#     def as_dict(self):
-#         """
-#         Returns the entry as a dictionary for serialization.
-#         """
-#         if self.meta_data:
-#             return {
-#                 "duration": self.duration,
-#                 "artist": self.meta_data.artist,
-#                 "album": self.meta_data.album,
-#                 "title": self.meta_data.title
-#             }
-#         return None
-
-
-#     def __str__(self):
-#         """
-#         String representation of the object.
-#         """
-#         time_start = SimpleUtil.fmt_time(self.start_unix)
-#         time_end = SimpleUtil.fmt_time(self.end_unix)
-#         track = self.source[-25:]
-#         return "SingleEntry #%s [%s - %s | %ssec | Source: ...%s]" % (str(self.id), time_start, time_end, self.duration, track)
-
+        return DB.session.query(PlaylistEntryMetaData).filter(PlaylistEntryMetaData.artificial_entry_id == artificial_playlistentry_id).first()
 
 
-# class SingleEntryMetaData(DB.Model, AuraDatabaseModel):
-#     """
-#     Metadata for a autonomous entry such as the artist and track name.
-#     """
-#     __tablename__ = "single_entry_metadata"
-
-#     id = Column(Integer, primary_key=True)
-#     single_entry_id = Column(Integer, ForeignKey("single_entry.id"))
-
-#     artist = Column(String(256))
-#     title = Column(String(256))
-#     album = Column(String(256))
-
-#     entry = relationship("SingleEntry", uselist=False, back_populates="meta_data")
-
-#     @staticmethod
-#     def select_metadata_for_entry(single_entry_id):
-#         return DB.session.query(SingleEntry).filter(SingleEntryMetaData.id == single_entry_id).first()
-
-
-
-
-
-#
-#   LEGACY CLASSES
-#
-
-
-
-# ------------------------------------------------------------------------------------------ #
-# class Schedule(DB.Model, AuraDatabaseModel):
-#     """
-#     One specific Schedule for a show on a timeslot
-#     """
-#     __tablename__ = 'schedule'
-#
-#     # primary and foreign keys
-#     schedule_start = Column(DateTime, primary_key=True)
-#
-#     schedule_end = Column(DateTime)
-#     schedule_id = Column(Integer) #, primary_key=True, autoincrement=False)
-#     show_id = Column(Integer)  # well, in fact not needed..
-#     show_name = Column(String(256))
-#     show_hosts = Column(String(256))
-#     funding_category = Column(String(256))
-#     comment = Column(String(512))
-#     languages = Column(String(256))
-#     type = Column(String(256))
-#     category = Column(String(256))
-#     topic = Column(String(256))
-#     musicfocus = Column(String(256))
-#
-#     is_repetition = Column(Boolean())
-#
-#     playlist_id = Column(Integer, ForeignKey("playlist.playlist_id"))
-#     timeslot_fallback_id = Column(Integer)
-#     show_fallback_id = Column(Integer)
-#     station_fallback_id = Column(Integer)
-#
-#     playlist = relationship("Playlist", foreign_keys=[playlist_id], lazy="joined")
-#  #    timeslot_fallback = relationship("Playlist", foreign_keys=[timeslot_fallback_id], lazy="joined")
-#  #    show_fallback = relationship("Playlist", foreign_keys=[show_fallback_id], lazy="joined")
-#  #    station_fallback = relationship("Playlist", foreign_keys=[station_fallback_id], lazy="joined")
-#
-#     @staticmethod
-#     def select_all():
-#          # fetching all entries
-#         all_entries = DB.session.query(Schedule).filter().order_by(Schedule.schedule_start).all()
-#         return all_entries
-#
-#     @staticmethod
-#     def select_by_id(id):
-#         entry = DB.session.query(Schedule).filter(Schedule.schedule_id == id).first()
-#         return entry
-#     @staticmethod
-#     def select_act_programme():
-#          #DB.session.query(Schedule).filter
-#          # fetching all from today to ..
-#         today = datetime.date.today()
-#         all_entries = DB.session.query(Schedule).filter(Schedule.schedule_start >= today).order_by(Schedule.schedule_start).all()
-#
-#         return all_entries
-#
-
-#
-#     @staticmethod
-#     def drop_the_future(timedelta):
-#         then = datetime.datetime.now() + timedelta
-#
-#         # is this really necessary?
-#         future_entries = DB.session.query(Schedule).filter(Schedule.schedule_start > then)
-#         for e in future_entries:
-#             e.delete()
-#         DB.session.commit()
-#
-#     def get_length(self):
-#         sec1 = int(datetime.datetime.strptime(self.start[0:16].replace(" ", "T"), "%Y-%m-%dT%H:%M").strftime("%s"))
-#         sec2 = int(datetime.datetime.strptime(self.end[0:16].replace(" ", "T"), "%Y-%m-%dT%H:%M").strftime("%s"))
-#         len = sec2 - sec1
-#         return len
-#
-#     # ------------------------------------------------------------------------------------------ #
-#     def __str__(self):
-#         return "ScheduleID: #" + str(self.schedule_id) + " Showname: " + self.show_name + " starts @ " + str(self.schedule_start)
-
-# ------------------------------------------------------------------------------------------ #
-
-
-
-# ------------------------------------------------------------------------------------------ #
-#class PlaylistEntry(DB.Model, AuraDatabaseModel):
-    # __tablename__ = 'playlist_entry'
-    #
-    # # primary and foreign keys
-    # playlist_id = Column(Integer, ForeignKey("playlist.playlist_id"), primary_key=True, nullable=False, autoincrement=True)
-    # entry_num = Column(Integer, primary_key=True, nullable=False, autoincrement=False)
-    #
-    # uri = Column(String(1024))
-    #
-    # source = ""
-    # cleansource = ""
-    # cleanprotocol = ""
-    # type = None
-    # fadeintimer = None
-    # fadeouttimer = None
-    # switchtimer = None
-    #
-    # meta_data = relationship("PlaylistEntryMetaData", primaryjoin="and_(PlaylistEntry.playlist_id==PlaylistEntryMetaData.playlist_id, PlaylistEntry.entry_num==PlaylistEntryMetaData.entry_num)", lazy="joined")
-    #
-    # # normal constructor
-    # def __init__(self, **kwargs):
-    #     super(PlaylistEntry, self).__init__(**kwargs)
-    #     self.calc_unix_times()
-    #     self.define_clean_source()
-    #
-    # # constructor like - called from sqlalchemy
-    # @orm.reconstructor
-    # def reconstructor(self):
-    #     self.calc_unix_times()
-    #     self.define_clean_source()
-    #     self.set_entry_type()
-    #
-    # def define_clean_source(self):
-    #     if self.uri is None:
-    #         return None
-    #
-    #     if self.uri.startswith("http"):
-    #         self.cleanprotocol = self.uri[:7]
-    #         self.cleansource = self.uri
-    #
-    #     elif self.uri.startswith("linein"):
-    #         self.cleanprotocol = self.uri[:9]
-    #         self.cleansource = self.uri[9:]
-    #
-    #     elif self.uri.startswith("pool") or self.uri.startswith("file") or self.uri.startswith("live"):
-    #         self.cleanprotocol = self.uri[:7]
-    #         self.cleansource = self.uri[7:]
-    #
-    #     elif self.uri.startswith("playlist"):
-    #         self.cleanprotocol = self.uri[:11]
-    #         self.cleansource = self.uri[11:]
-    #
-    #     else:
-    #         self.logger.error("Unknown source protocol")
-    #
-    # def set_entry_type(self):
-    #     if self.uri.startswith("http"):
-    #         self.type = ScheduleEntryType.HTTP
-    #     if self.uri.startswith("pool") or self.uri.startswith("playlist") or self.uri.startswith("file"):
-    #         self.type = ScheduleEntryType.FILESYSTEM
-    #     if self.uri.startswith("live") or self.uri.startswith("linein"):
-    #         if self.cleansource == "0":
-    #             self.type = ScheduleEntryType.LIVE_0
-    #         elif self.cleansource == "1":
-    #             self.type = ScheduleEntryType.LIVE_1
-    #         elif self.cleansource == "2":
-    #             self.type = ScheduleEntryType.LIVE_2
-    #         elif self.cleansource == "3":
-    #             self.type = ScheduleEntryType.LIVE_3
-    #         elif self.cleansource == "4":
-    #             self.type = ScheduleEntryType.LIVE_4
-
-
-
-
-    # def calc_unix_times(self):
-    #     if self.entry_start is not None:
-    #         self.entry_start_unix = time.mktime(self.entry_start.timetuple())
-    #
-    #
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def select_all():
-    #     # fetching all entries
-    #     all_entries = DB.session.query(Playlist).filter(Playlist.fallback_type == 0).order_by(Playlist.entry_start).all()
-    #
-    #     cnt = 0
-    #     for entry in all_entries:
-    #         entry.programme_index = cnt
-    #         cnt = cnt + 1
-    #
-    #     return all_entries
-    #
-    # @staticmethod
-    # def select_act_programme(include_act_playing = True):
-    #     # fetching all from today to ..
-    #     today = datetime.date.today()
-    #     all_entries = DB.session.query(Playlist).filter(Playlist.entry_start >= today, Playlist.fallback_type == 0).order_by(Playlist.entry_start).all()
-    #
-    #     cnt = 0
-    #     for entry in all_entries:
-    #         entry.programme_index = cnt
-    #         cnt = cnt + 1
-    #
-    #     return all_entries
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def truncate():
-    #     all_entries = DB.session.query(Playlist).filter().order_by(Playlist.entry_start).all()
-    #
-    #     for a in all_entries:
-    #         a.delete()
-    #     DB.session.commit()
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def select_next_manual_entry_num():
-    #
-    #     max_manual_entry_num = DB.session.query(func.max(Playlist.entry_num)).filter(Playlist.schedule_id == 0).first()
-    #
-    #     if max_manual_entry_num[0] is None:
-    #         return 0
-    #     else:
-    #         return int(max_manual_entry_num[0])+1
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def select_upcoming(datefrom=datetime.datetime.now()):
-    #     upcomingtracks = DB.session.query(Playlist).filter(Playlist.entry_start > datefrom).order_by(Playlist.entry_start).all()
-    #     return upcomingtracks
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def select_one(playlist_id, entry_num):
-    #     return DB.session.query(Playlist).filter(Playlist.playlist_id == playlist_id, Playlist.entry_num == entry_num).first()
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def select_one_playlist_entry_for_show(schedule_id, playlist_type, entry_num):
-    #     return DB.session.query(Playlist).filter(Playlist.schedule_id == schedule_id, Playlist.fallback_type == playlist_type, Playlist.entry_num == entry_num).first()
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # @staticmethod
-    # def select_playlist(playlist_id):
-    #     return DB.session.query(Playlist).filter(Playlist.playlist_id == playlist_id).order_by(Playlist.entry_start).all()
-    #
-    # @staticmethod
-    # def drop_the_future(timedelta):
-    #     then = datetime.datetime.now() + timedelta
-    #     #DB.session.delete(ScheduleEntry).filter(ScheduleEntry.entry_start >= then)
-    #
-    #     # is this really necessary?
-    #     future_entries = DB.session.query(Playlist).filter(Playlist.entry_start > then)
-    #     for e in future_entries:
-    #         e.delete()
-    #     DB.session.commit()
-    #
-    # def getChannel(self):
-    #     if self.type == self.type.FILESYSTEM:
-    #         return "fs"
-    #
-    #     if self.type == self.type.LIVE_0 or self.type == self.type.LIVE_1 or self.type == self.type.LIVE_2 or self.type == self.type.LIVE_3 or self.type == self.type.LIVE_4:
-    #         return "aura_linein_"+self.cleansource # .cleanprotocol[8]
-    #
-    #     if self.type == self.type.HTTP:
-    #         return "http"
-    #
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # def __str__(self):
-    #     return "Showentry starts @ " + str(self.entry_start) + " and plays " + self.source
-
-
-# class ScheduleEntryFile(DB.Model, AuraDatabaseModel):
-#     __tablename__ = 'schedule_entry_file'
-#
-#     # primary and foreign keys
-#     file_id = Column(Integer, primary_key=True, nullable=False, autoincrement=False)
-#     playlist_id = Column(Integer) #, ForeignKey("schedule_entry.playlist_id")) # primary_key=True, nullable=False, autoincrement=False)
-#     entry_num = Column(Integer) # , ForeignKey("schedule_entry.entry_num")) # primary_key=True, nullable=False, autoincrement=False)
-#
-#     ForeignKeyConstraint(["playlist_id", "entry_num"], ["schedule_entry.playlist_id", "schedule_entry.entry_num"])
-#
-#     show = Column(String(512))
-#     size = Column(Integer)
-#     duration = Column(Integer)
-#
-# class ScheduleEntryFileMetaData(DB.Model, AuraDatabaseModel):
-#     __tablename__ = "schedule_entry_file_metadata"
-#
-#     metadata_id = Column(Integer, primary_key=True, nullable=False, autoincrement=True)
-#     file_id = Column(Integer, ForeignKey("schedule_entry_file.file_id"))
-#
-#     artist = Column(String(256))
-#     title = Column(String(256))
-#     album = Column(String(256))
-#
-# # ------------------------------------------------------------------------------------------ #
-# class TrackService(DB.Model, AuraDatabaseModel):
-#     __tablename__ = 'trackservice'
-#
-#     trackservice_id = Column(Integer, primary_key=True, autoincrement=True)
-#     schedule_entry_id = Column(Integer, ForeignKey("schedule_entry.id"))
-#     playlist_id = Column(Integer, nullable=False)
-#     entry_num = Column(Integer, nullable=False)
-#
-#     source = Column(String(255), nullable=False)
-#     start = Column(DateTime, nullable=False, default=func.now())
-
-#    __table_args__ = (
-#        ForeignKeyConstraint(['playlist_id', 'entry_num'], ['schedule_entry.playlist_id', 'schedule_entry.entry_num']),
-#    )
-#    schedule_entry = relationship("ScheduleEntry", primaryjoin="and_(TrackService.playlist_id==ScheduleEntry.playlist_id, TrackService.entry_num==ScheduleEntry.entry_num)", lazy="joined")
-
-    #schedule = relationship("Schedule", foreign_keys=[schedule_id], lazy="joined")
-    # trackservice_entry = relationship("ScheduleEntry", foreign_keys=[playlist_id, entry_num], lazy="joined")
-    # schedule_entry = relationship("ScheduleEntry", primaryjoin="and_(TrackService.schedule_entry_id==ScheduleEntry.id)", lazy="joined")
-    #
-    # @staticmethod
-    # # ------------------------------------------------------------------------------------------ #
-    # def select_one(trackservice_id):
-    #     return DB.session.query(TrackService).filter(TrackService.trackservice_id == trackservice_id).first()
-    #
-    # @staticmethod
-    # # ------------------------------------------------------------------------------------------ #
-    # def select_by_day(day):
-    #     day_plus_one = day + datetime.timedelta(days=1)
-    #     tracks = DB.session.query(TrackService).filter(TrackService.start >= str(day), TrackService.start < str(day_plus_one)).all()
-    #     return tracks
-    #
-    # @staticmethod
-    # # ------------------------------------------------------------------------------------------ #
-    # def select_by_range(from_day, to_day):
-    #     tracks = DB.session.query(TrackService).filter(TrackService.start >= str(from_day),
-    #                                                    TrackService.start < str(to_day)).all()
-    #     return tracks
-    #
-    # # ------------------------------------------------------------------------------------------ #
-    # def __str__(self):
-    #     return "TrackServiceID: #" + str(self.trackservice_id) + " playlist_id: " + str(self.playlist_id) + " started @ " + str(self.start) + " and played " + self.source
-
-# ------------------------------------------------------------------------------------------ #
-# class TrackServiceSchedule(db.Model, AuraDatabaseModel):
-#     """
-#     Trackservice is tracking every schedule.
-#     """
-#     __tablename__ = 'trackservice_schedule'
-#
-#     # primary and foreign keys
-#     ts_schedule_id = Column(Integer, primary_key=True, autoincrement=True)
-#     schedule_id = Column(Integer, ForeignKey("schedule.schedule_id"))
-#
-#     schedule = relationship("Schedule", foreign_keys=[schedule_id], lazy="joined")
-#
-#     # ------------------------------------------------------------------------------------------ #
-#     @staticmethod
-#     def select_one(schedule_id):
-#         # damn BAND-AID
-#         # db.session.commit()
-#
-#         return db.session.query(ScheduleEntry).filter(TrackServiceSchedule.schedule_id == schedule_id).first()
-#
-# # ------------------------------------------------------------------------------------------ #
-# class TrackServiceScheduleEntry(db.Model, AuraDatabaseModel):
-#     """
-#     And a schedule can have multiple entries
-#     """
-#     __tablename__ = 'trackservice_entry'
-#
-#     # primary and foreign keys. the foreign keys here can be null, because of fallback stuff
-#     ts_entry_id = Column(Integer, primary_key=True, autoincrement=True)
-#     ts_schedule_id = Column(Integer, ForeignKey("trackservice_schedule.ts_schedule_id"), nullable=True)
-#     playlist_id = Column(Integer, nullable=True)
-#     entry_num = Column(Integer, nullable=True)
-#
-#     fallback = Column(Boolean, default=False)
-#     fallback_start = Column(DateTime, nullable=True, default=None)
-#     source = Column(String(256), nullable=True, default=None)
-#
-#     # foreign key definitions
-#     __table_args__ = (
-#         ForeignKeyConstraint(['playlist_id', 'entry_num'], ['schedule_entry.playlist_id', 'schedule_entry.entry_num']),
-#     )
-#
-#     trackservice_schedule = relationship("TrackServiceSchedule", foreign_keys=[ts_schedule_id], lazy="joined")
-#     #trackservice_entry = relationship("ScheduleEntry", foreign_keys=[playlist_id, entry_num], lazy="joined")
-#     trackservice_entry = relationship("ScheduleEntry", primaryjoin="and_(TrackServiceScheduleEntry.playlist_id==ScheduleEntry.playlist_id, TrackServiceScheduleEntry.entry_num==ScheduleEntry.entry_num)" , lazy="joined")
-#
-#     @staticmethod
-#     def select_all():
-#         return db.session.query(TrackServiceScheduleEntry).filter().all()
-
-# AuraDatabaseModel.recreate_db(systemexit=True)
diff --git a/modules/plugins/trackservice.py b/modules/plugins/trackservice.py
index 5cefc667..f12d3154 100644
--- a/modules/plugins/trackservice.py
+++ b/modules/plugins/trackservice.py
@@ -85,7 +85,7 @@ class TrackServiceHandler():
         """
         Posts the current and next show information to the Engine API.
         """
-        current_playlist = self.soundsystem.scheduler.get_active_playlist()           
+        current_playlist = self.soundsystem.scheduler.get_active_playlist()
         current_schedule = current_playlist.schedule
         next_schedule = self.soundsystem.scheduler.get_next_schedules(1)
         if next_schedule: next_schedule = next_schedule[0]
diff --git a/modules/scheduling/calendar.py b/modules/scheduling/calendar.py
index 9571b7e3..f920fa55 100644
--- a/modules/scheduling/calendar.py
+++ b/modules/scheduling/calendar.py
@@ -124,8 +124,6 @@ class AuraCalendarService(threading.Thread):
                         msg = "Schedule #%s has been deleted remotely. Since the scheduling window has already started, it won't be deleted locally." % local_schedule.schedule_id
                         self.logger.warn(SimpleUtil.red(msg))
 
-                
-
             # Process fetched schedules    
             for schedule in fetched_schedule_data:
 
@@ -149,8 +147,6 @@ class AuraCalendarService(threading.Thread):
                 if schedule_db.station_fallback_id:
                     self.store_playlist(schedule_db, schedule_db.station_fallback_id, schedule["station_fallback"], PlaylistType.STATION.id)
 
-
-
                 result.append(schedule_db)
 
             # Release the mutex
@@ -195,10 +191,6 @@ class AuraCalendarService(threading.Thread):
         schedule_db.topic = schedule["show_topics"]
         schedule_db.musicfocus = schedule["show_musicfocus"]
 
-        # if schedule["playlist_id"] is None:
-        #     # FIXME Manually assigned playlist ID.
-        #     schedule["playlist_id"] = 1
-
         schedule_db.playlist_id = schedule["playlist_id"]
         schedule_db.schedule_fallback_id = schedule["schedule_fallback_id"]
         schedule_db.show_fallback_id = schedule["show_fallback_id"]
@@ -236,7 +228,7 @@ class AuraCalendarService(threading.Thread):
             playlist_db.entry_count = 0
 
         playlist_db.store(havetoadd, commit=True)
-
+      
         if playlist_db.entry_count > 0:
             self.store_playlist_entries(schedule_db, playlist_db, fetched_playlist)
 
@@ -251,6 +243,10 @@ class AuraCalendarService(threading.Thread):
         entry_num = 0
         time_marker = playlist_db.start_unix
 
+        # If existing playlist entries are beyond the count of the new playlist entries,
+        # then delete those first
+        self.delete_orphaned_entries(playlist_db, fetched_playlist)  
+
         for entry in fetched_playlist["entries"]:
             entry_db = PlaylistEntry.select_playlistentry_for_playlist(playlist_db.artificial_id, entry_num)
             havetoadd = False
@@ -289,6 +285,25 @@ class AuraCalendarService(threading.Thread):
 
 
 
+    def delete_orphaned_entries(self, playlist_db, fetched_playlist):
+        """
+        Deletes all playlist entries which are beyond the current playlist's `entry_count`.
+        Such entries might be existing due to a remotely changed playlist, which now has
+        less entries than before.
+        """
+        new_last_idx = len(fetched_playlist["entries"])
+        existing_last_idx = PlaylistEntry.count_entries(playlist_db.artificial_id)-1
+
+        if existing_last_idx < new_last_idx:
+            return 
+
+        for entry_num in range(new_last_idx, existing_last_idx+1, 1):
+            PlaylistEntry.delete_entry(playlist_db.artificial_id, entry_num)            
+            self.logger.info(SimpleUtil.yellow("Deleted playlist entry %s:%s" % (playlist_db.artificial_id, entry_num)))
+            entry_num += 1
+
+
+
     def store_playlist_entry_metadata(self, entry_db, metadata):
         """
         Stores the meta-data for a PlaylistEntry.
@@ -301,20 +316,20 @@ class AuraCalendarService(threading.Thread):
 
         metadata_db.artificial_entry_id = entry_db.artificial_id
 
-        if "artist" not in metadata:
-            self.logger.warning("Artist not found in metadata for track '%s'. Setting to ''" % entry_db.source)
-            metadata_db.artist = ""
-        else:
+        if "artist" in metadata:
             metadata_db.artist = metadata["artist"]
+        else:
+            metadata_db.artist = ""
         
         if "album" in metadata:
             metadata_db.album = metadata["album"]
+        else:
+            metadata_db.album = ""
 
-        if "title" not in metadata:
-            self.logger.warning("Title not found in metadata for track '%s'. Setting to 'n/a'" % entry_db.source)
-            metadata_db.title = ""
+        if "title" in metadata:
+            metadata_db.title = metadata["title"]
         else:
-            metadata_db.artist = metadata["title"]
+            metadata_db.title = ""
 
         metadata_db.store(havetoadd, commit=True)
 
-- 
GitLab