episoder 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
episoder/__init__.py ADDED
File without changes
episoder/database.py ADDED
@@ -0,0 +1,134 @@
1
+ # episoder, https://code.ott.net/episoder
2
+ #
3
+ # Copyright (C) 2004-2024 Stefan Ott. All rights reserved.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import logging
19
+
20
+ from datetime import datetime
21
+
22
+ from sqlalchemy import Column, Integer, Text, Date, ForeignKey, Sequence
23
+ from sqlalchemy import DateTime, Boolean
24
+ from sqlalchemy.orm import backref, declarative_base, relationship
25
+
26
+ Base = declarative_base()
27
+
28
+
29
+ class Show(Base):
30
+
31
+ RUNNING = 1
32
+ SUSPENDED = 2
33
+ ENDED = 3
34
+
35
+ __tablename__ = "shows"
36
+
37
+ id = Column("show_id", Integer, Sequence("shows_show_id_seq"),
38
+ primary_key=True)
39
+ name = Column("show_name", Text)
40
+ url = Column(Text, unique=True)
41
+ updated = Column(DateTime)
42
+ enabled = Column(Boolean)
43
+ status = Column(Integer, default=RUNNING)
44
+
45
+ def __init__(self, name, url="", updated=datetime.fromtimestamp(0)):
46
+
47
+ self.name = name
48
+ self.url = url
49
+ self.updated = updated
50
+ self.status = Show.RUNNING
51
+ self.episodes = []
52
+ self.enabled = True
53
+
54
+ def __str__(self):
55
+
56
+ return f"Show: {self.name}"
57
+
58
+ def __repr__(self):
59
+
60
+ return f'Show("{self.name}", "{self.url}")'
61
+
62
+ def __eq__(self, other):
63
+
64
+ return self.name == other.name and self.url == other.url
65
+
66
+ def remove_episodes_before(self, store, date):
67
+
68
+ logging.debug("Removing episodes from before %s", date)
69
+ store.remove_before(date, show=self)
70
+
71
+
72
+ class Episode(Base):
73
+
74
+ __tablename__ = "episodes"
75
+
76
+ show_id = Column(Integer, ForeignKey(Show.id), primary_key=True)
77
+ episode = Column("num", Integer, primary_key=True)
78
+ airdate = Column(Date)
79
+ season = Column(Integer, primary_key=True)
80
+ title = Column(Text)
81
+ totalnum = Column(Integer)
82
+ prodnum = Column(Text)
83
+ notified = Column(Date)
84
+ show = relationship(Show, backref=backref("episodes", cascade="all"))
85
+
86
+ # pylint: disable=too-many-arguments
87
+ def __init__(self, name, season, episode, aired, prodnum, total):
88
+
89
+ self.title = name
90
+ self.season = int(season)
91
+ self.episode = int(episode)
92
+ self.airdate = aired
93
+ self.prodnum = prodnum
94
+ self.totalnum = int(total)
95
+
96
+ def __lt__(self, other):
97
+
98
+ return ((self.season < other.season)
99
+ or ((self.season == other.season)
100
+ and (self.episode < other.episode)))
101
+
102
+ def __str__(self):
103
+
104
+ return f"{self.show.name} {self.season}x{self.episode:02d}: " \
105
+ f"{self.title}"
106
+
107
+ def __repr__(self):
108
+
109
+ d = self.airdate
110
+
111
+ return f'Episode("{self.title}", {self.season}, {self.episode}, ' \
112
+ f'date({d.year}, {d.month}, {d.day}), "{self.prodnum}", ' \
113
+ f'{self.totalnum})'
114
+
115
+ def __eq__(self, other):
116
+
117
+ return (self.show_id == other.show_id
118
+ and self.season == other.season
119
+ and self.episode == other.episode)
120
+
121
+
122
+ class Meta(Base):
123
+
124
+ __tablename__ = "meta"
125
+ key = Column(Text, primary_key=True)
126
+ value = Column(Text)
127
+
128
+ def __str__(self):
129
+
130
+ return f"Meta: {self.key} = {self.value}"
131
+
132
+ def __repr__(self):
133
+
134
+ return str(self)
episoder/episoder.py ADDED
@@ -0,0 +1,292 @@
1
+ # episoder, https://code.ott.net/episoder
2
+ #
3
+ # Copyright (C) 2004-2024 Stefan Ott. All rights reserved.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ import logging
19
+ from datetime import date, timedelta
20
+
21
+ import sqlite3
22
+ from sqlalchemy import create_engine, or_, and_, select, inspect
23
+ from sqlalchemy.orm import sessionmaker
24
+
25
+ from .database import Episode, Show, Meta
26
+
27
+
28
+ class Database:
29
+
30
+ def __init__(self, path):
31
+
32
+ self._path = path
33
+ self.logger = logging.getLogger("Database")
34
+
35
+ self.open()
36
+ self._initdb()
37
+
38
+ def __str__(self):
39
+
40
+ return f"Episoder Database at {self._path}"
41
+
42
+ def __repr__(self):
43
+
44
+ return f"Database({self._path})"
45
+
46
+ def _initdb(self):
47
+
48
+ inspector = inspect(self.engine)
49
+
50
+ # Initialize the database if all tables are missing
51
+ names = [Show, Episode, Meta]
52
+ tables = [t for t in names if inspector.has_table(t.__table__.name)]
53
+
54
+ if len(tables) < 1:
55
+ Show.__table__.create(bind=self.engine)
56
+ Episode.__table__.create(bind=self.engine)
57
+ Meta.__table__.create(bind=self.engine)
58
+ self.set_schema_version(4)
59
+
60
+ def open(self):
61
+
62
+ if self._path.find("://") > -1:
63
+ self.engine = create_engine(self._path)
64
+ else:
65
+ self.engine = create_engine(f"sqlite:///{self._path}")
66
+
67
+ sm = sessionmaker(self.engine)
68
+ self.session = sm()
69
+ self.session.begin()
70
+
71
+ def close(self):
72
+
73
+ self.session.commit()
74
+ self.session.close()
75
+ self.engine.dispose()
76
+
77
+ def set_schema_version(self, version):
78
+
79
+ meta = Meta()
80
+ meta.key = "schema"
81
+ meta.value = f"{version}"
82
+
83
+ with self.session.begin_nested():
84
+ self.session.merge(meta)
85
+
86
+ def get_schema_version(self):
87
+
88
+ inspector = inspect(self.engine)
89
+ if not inspector.has_table(Meta.__table__.name):
90
+ return 1
91
+
92
+ res = self.session.scalars(
93
+ select(Meta).where(Meta.key == "schema")).first()
94
+ if res:
95
+ return int(res.value)
96
+
97
+ return 0
98
+
99
+ def clear(self):
100
+
101
+ episodes = self.session.query(Episode).all()
102
+
103
+ for episode in episodes:
104
+ self.session.delete(episode)
105
+
106
+ self.session.flush()
107
+
108
+ def migrate(self):
109
+
110
+ schema_version = self.get_schema_version()
111
+ self.logger.debug("Found schema version %s", schema_version)
112
+
113
+ if schema_version < 0:
114
+
115
+ self.logger.debug("Automatic schema updates disabled")
116
+ return
117
+
118
+ if schema_version == 1:
119
+
120
+ # Upgrades from version 1 are rather harsh, we
121
+ # simply drop and re-create the tables
122
+ self.logger.debug("Upgrading to schema version 2")
123
+
124
+ upgrade = sqlite3.connect(self._path)
125
+ upgrade.execute("DROP TABLE episodes")
126
+ upgrade.execute("DROP TABLE shows")
127
+ upgrade.close()
128
+
129
+ Show.__table__.create(bind=self.engine)
130
+ Episode.__table__.create(bind=self.engine)
131
+ Meta.__table__.create(bind=self.engine)
132
+
133
+ schema_version = 4
134
+ self.set_schema_version(schema_version)
135
+
136
+ if schema_version == 2:
137
+
138
+ # Add two new columns to the shows table
139
+ self.logger.debug("Upgrading to schema version 3")
140
+
141
+ # We can only do this with sqlite databases
142
+ assert self.engine.driver == "pysqlite"
143
+
144
+ self.close()
145
+
146
+ upgrade = sqlite3.connect(self._path)
147
+ upgrade.execute("ALTER TABLE shows "
148
+ "ADD COLUMN enabled TYPE boolean")
149
+ upgrade.execute("ALTER TABLE shows "
150
+ "ADD COLUMN status TYPE integer")
151
+ upgrade.close()
152
+
153
+ self.open()
154
+ schema_version = 3
155
+ self.set_schema_version(schema_version)
156
+
157
+ if schema_version == 3:
158
+
159
+ # Add a new column to the episodes table
160
+ self.logger.debug("Upgrading to schema version 4")
161
+
162
+ # We can only do this with sqlite databases
163
+ assert self.engine.driver == "pysqlite"
164
+
165
+ self.close()
166
+
167
+ upgrade = sqlite3.connect(self._path)
168
+ upgrade.execute("ALTER TABLE episodes "
169
+ "ADD COLUMN notified TYPE date")
170
+ upgrade.close()
171
+
172
+ self.open()
173
+ schema_version = 4
174
+ self.set_schema_version(schema_version)
175
+
176
+ def get_expired_shows(self, today=date.today()):
177
+
178
+ delta_running = timedelta(2) # 2 days
179
+ delta_suspended = timedelta(7) # 1 week
180
+ delta_ended = timedelta(14) # 2 weeks
181
+
182
+ shows = self.session.query(Show).filter(or_(
183
+ and_(
184
+ Show.enabled,
185
+ Show.status == Show.RUNNING,
186
+ Show.updated < today - delta_running
187
+ ),
188
+ and_(
189
+ Show.enabled,
190
+ Show.status == Show.SUSPENDED,
191
+ Show.updated < today - delta_suspended
192
+ ),
193
+ and_(
194
+ Show.enabled,
195
+ Show.status == Show.ENDED,
196
+ Show.updated < today - delta_ended
197
+ )
198
+ ))
199
+
200
+ return shows.all()
201
+
202
+ def get_enabled_shows(self):
203
+
204
+ shows = self.session.query(Show).filter(Show.enabled)
205
+ return shows.all()
206
+
207
+ def get_show_by_url(self, url):
208
+
209
+ shows = self.session.query(Show).filter(Show.url == url)
210
+
211
+ if shows.count() < 1:
212
+ return None
213
+
214
+ return shows.first()
215
+
216
+ def get_show_by_id(self, show_id):
217
+
218
+ return self.session.scalars(
219
+ select(Show).where(Show.id == show_id)).first()
220
+
221
+ def add_show(self, show):
222
+
223
+ show = self.session.merge(show)
224
+ self.session.flush()
225
+ return show
226
+
227
+ def remove_show(self, show_id):
228
+
229
+ show = self.session.scalars(
230
+ select(Show).where(Show.id == show_id)).first()
231
+
232
+ if not show:
233
+ self.logger.error("No such show")
234
+ return
235
+
236
+ episodes = self.session.scalars(select(Episode))
237
+
238
+ hits = [e for e in episodes if e.show_id == show.id]
239
+ for episode in hits:
240
+ self.session.delete(episode)
241
+
242
+ self.session.delete(show)
243
+ self.session.flush()
244
+
245
+ def get_shows(self):
246
+
247
+ return self.session.query(Show).all()
248
+
249
+ def add_episode(self, episode, show):
250
+
251
+ episode.show_id = show.id
252
+ self.session.merge(episode)
253
+ self.session.flush()
254
+
255
+ def get_episodes(self, basedate=date.today(), n_days=0):
256
+
257
+ enddate = basedate + timedelta(n_days)
258
+
259
+ return self.session.query(Episode). \
260
+ filter(Episode.airdate >= basedate). \
261
+ filter(Episode.airdate <= enddate). \
262
+ order_by(Episode.airdate).all()
263
+
264
+ def search(self, search):
265
+
266
+ q = self.session.query(Episode).join(Episode.show)
267
+ return q.filter(or_(
268
+ Episode.title.like(f"%%{search}%%"),
269
+ Show.name.like(f"%%{search}%%"))). \
270
+ order_by(Episode.airdate.asc()).all()
271
+
272
+ def commit(self):
273
+
274
+ self.session.commit()
275
+ self.session.begin()
276
+
277
+ def rollback(self):
278
+
279
+ self.session.rollback()
280
+ self.session.begin()
281
+
282
+ def remove_before(self, then, show=None):
283
+
284
+ eps = self.session.query(Episode).filter(Episode.airdate < then)
285
+
286
+ if show:
287
+ eps = eps.filter(Episode.show == show)
288
+
289
+ for episode in eps:
290
+ self.session.delete(episode)
291
+
292
+ self.commit()