database.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. # This file is part of stov, written by Helmut Pozimski 2012-2021.
  2. #
  3. # stov is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, version 2 of the License.
  6. #
  7. # stov is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License
  13. # along with stov. If not, see <http://www.gnu.org/licenses/>.
  14. # -*- coding: utf8 -*-
  15. """This module is responsible for all database related operations."""
  16. import logging
  17. import sqlite3
  18. from lib_stov import generic_video
  19. from lib_stov import stov_exceptions
  20. from lib_stov import subscription
  21. LOGGER = logging.getLogger("stov")
  22. class Db:
  23. """This class is used to construct the module which will take care of all
  24. database related operations like opening the database, reading from and
  25. writing to it.
  26. """
  27. _instance = None
  28. def __init__(self, path, version):
  29. """Constructor of the db class, populates the object with the relevant
  30. attributes, connects to the database and creates it if asked to.
  31. """
  32. self.__path = path
  33. self.__version = version
  34. try:
  35. self.__connection = sqlite3.connect(self.__path)
  36. except sqlite3.OperationalError as exc:
  37. raise stov_exceptions.DBConnectionFailedException() from exc
  38. else:
  39. self.__cursor = self.__connection.cursor()
  40. def __del__(self):
  41. """Destructor, closes the connection to the database."""
  42. self.__connection.close()
  43. def __new__(cls, *args, **kwargs):
  44. if not Db._instance:
  45. Db._instance = super(Db, cls).__new__(cls)
  46. return Db._instance
  47. def _execute_statement(self, statement, argument=None):
  48. """Executes a statement, works as a wrapper around cursor execute."""
  49. try:
  50. if not argument:
  51. LOGGER.debug(_("Executing STATEMENT: %s"), statement)
  52. result = self.__cursor.execute(statement)
  53. else:
  54. LOGGER.debug(_("Executing STATEMENT: %s with arguments %s"),
  55. statement, argument)
  56. result = self.__cursor.execute(statement, argument)
  57. except sqlite3.OperationalError as error:
  58. LOGGER.debug(error)
  59. raise stov_exceptions.DBWriteAccessFailedException()
  60. else:
  61. self.__connection.commit()
  62. return result
  63. @staticmethod
  64. def get_instance():
  65. """ Return the singleton instance of Db"""
  66. return Db._instance
  67. def populate(self):
  68. """Populates the database with the initial structure."""
  69. self._execute_statement("""CREATE TABLE subscriptions (
  70. id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  71. title TEXT,
  72. name TEXT,
  73. type TEXT,
  74. searchstring TEXT,
  75. directory TEXT,
  76. disabled INTEGER DEFAULT 0,
  77. site INTEGER NOT NULL
  78. );""")
  79. self._execute_statement("""CREATE TABLE videos (
  80. id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  81. title TEXT,
  82. ytid TEXT,
  83. subscription_id INTEGER,
  84. downloaded INT,
  85. failcnt INTEGER DEFAULT 0
  86. );""")
  87. self._execute_statement("""CREATE TABLE sites(
  88. id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  89. title TEXT);""")
  90. def update(self):
  91. """Updates the database structure to match the current version"""
  92. if int(self.__version) == 1:
  93. # Changes between version 1 and 2
  94. self._execute_statement("ALTER TABLE videos ADD COLUMN failcnt \
  95. INTEGER DEFAULT 0;")
  96. self.__version = 2
  97. if int(self.__version) == 2:
  98. # Changes between version 2 and 3
  99. self._execute_statement("ALTER TABLE subscriptions ADD COLUMN"
  100. " disabled INTEGER DEFAULT 0;")
  101. self._execute_statement("UPDATE subscriptions SET disabled=0;")
  102. self.__version = 3
  103. if int(self.__version) == 3:
  104. # Pseudo version without changes to the database structure,
  105. # converts existing channel subscriptions into user ones.
  106. self._execute_statement("UPDATE subscriptions SET type='user' "
  107. "WHERE type='channel'")
  108. self.__version = 4
  109. if int(self.__version) == 4:
  110. # Changes between version 4 and 5
  111. self._execute_statement("""CREATE TABLE videos_backup (
  112. id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  113. title TEXT,
  114. ytid TEXT,
  115. subscription_id INTEGER,
  116. downloaded INT,
  117. failcnt INTEGER DEFAULT 0
  118. );""")
  119. self._execute_statement("INSERT INTO videos_backup SELECT"
  120. " id, title, ytid, subscription_id, "
  121. "downloaded, failcnt FROM videos;")
  122. self._execute_statement("DROP TABLE videos;")
  123. self._execute_statement("ALTER TABLE videos_backup RENAME TO "
  124. "videos;")
  125. self.__version = 5
  126. if int(self.__version) == 5:
  127. self._execute_statement("""CREATE TABLE sites(
  128. id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  129. title TEXT);""")
  130. self._execute_statement("""ALTER TABLE subscriptions ADD
  131. COLUMN site INTEGER;""")
  132. self.add_site("youtube")
  133. self._execute_statement("""UPDATE subscriptions SET site=1;""")
  134. self._execute_statement("""CREATE TABLE subscriptions_backup (
  135. id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  136. title TEXT,
  137. name TEXT,
  138. type TEXT,
  139. searchstring TEXT,
  140. directory TEXT,
  141. disabled INTEGER DEFAULT 0,
  142. site INTEGER NOT NULL
  143. );""")
  144. self._execute_statement("INSERT INTO subscriptions_backup SELECT"
  145. " id, title, name, type, "
  146. "searchstring, directory, disabled, "
  147. "site FROM subscriptions;")
  148. self._execute_statement("DROP TABLE subscriptions;")
  149. self._execute_statement("ALTER TABLE subscriptions_backup "
  150. "RENAME TO subscriptions;")
  151. self.__version = 6
  152. def get_version(self):
  153. """Simple getter method, returns the DB version"""
  154. return self.__version
  155. def delete_subscription(self, sub_id):
  156. """Deletes a subscription and all associated videos from the
  157. database
  158. """
  159. checkquery = "SELECT * FROM subscriptions WHERE id=?"
  160. checkresult = self._execute_statement(checkquery, (sub_id,))
  161. if not checkresult.fetchall():
  162. raise stov_exceptions.SubscriptionNotFoundException()
  163. delete_videos = "DELETE FROM videos WHERE subscription_id=?"
  164. self._execute_statement(delete_videos, (sub_id,))
  165. delete_subscription = "DELETE FROM subscriptions WHERE id=?"
  166. self._execute_statement(delete_subscription, (sub_id,))
  167. def get_videos(self, subscription_id):
  168. """Gets all videos of a given subscription id from the database and
  169. returns them
  170. """
  171. videos_list = []
  172. video_query = "SELECT id, title, ytid, downloaded, " \
  173. "failcnt FROM videos WHERE subscription_id=?"
  174. cursor = self._execute_statement(video_query, (subscription_id,))
  175. result = cursor.fetchall()
  176. for video in result:
  177. videos_list.append(
  178. generic_video.Video(title=video[1], site_id=video[2],
  179. downloaded=video[3], failcount=video[4],
  180. video_id=video[0]))
  181. return videos_list
  182. def video_in_database(self, ytid):
  183. """Checks if the video with a given youtube id already exists in the
  184. database
  185. """
  186. video_query = "SELECT id FROM videos WHERE ytid=?"
  187. cursor = self._execute_statement(video_query, (ytid,))
  188. result = cursor.fetchall()
  189. if not result:
  190. return False
  191. return True
  192. def insert_video(self, video, subscription_id):
  193. """Inserts a video with the given data into the database"""
  194. video_insert = "INSERT INTO videos (title, ytid, \
  195. subscription_id, downloaded) VALUES \
  196. (?, ?, ?, ?)"
  197. insert_data = (video.title, video.video_id,
  198. subscription_id, 0)
  199. self._execute_statement(video_insert, insert_data)
  200. def insert_subscription(self, data):
  201. """Inserts a subscription with the given data into the database"""
  202. subscription_insert = """INSERT INTO subscriptions (title, type,
  203. searchstring, directory, name, disabled, site)
  204. VALUES (?, ?, ?, ?, ?, ?, ?)"""
  205. self._execute_statement(subscription_insert, data)
  206. subscription_getid = "SELECT id from subscriptions where title=?"
  207. query_cursor = self._execute_statement(subscription_getid, (data[0],))
  208. subscription_id = query_cursor.fetchone()[0]
  209. return subscription_id
  210. def update_video_download_status(self, video_id, status):
  211. """Updates the download status of a video in the database"""
  212. update_statement = "UPDATE videos SET downloaded = ? WHERE id = ?"
  213. self._execute_statement(update_statement, (status, video_id))
  214. def disable_failed_video(self, video_id):
  215. """Disables a video in the database"""
  216. update_statement = "UPDATE videos SET downloaded = -1 WHERE id = ?"
  217. self._execute_statement(update_statement, (video_id,))
  218. def update_video_fail_count(self, count, video_id):
  219. """Updates the fail count of a video in the database"""
  220. update_statement = "UPDATE videos SET failcnt = ? WHERE id = ?"
  221. self._execute_statement(update_statement, (count, video_id))
  222. def delete_video(self, video_id):
  223. """Deletes a video from the database"""
  224. delete_statement = "DELETE FROM videos where id = ?"
  225. self._execute_statement(delete_statement, (video_id,))
  226. def get_subscription(self, sub_id):
  227. """Retrieves a specific subscription from the database"""
  228. sub_query = """SELECT subscriptions.id, subscriptions.title,
  229. type, name,searchstring, directory,disabled, sites.title FROM
  230. subscriptions INNER JOIN sites ON subscriptions.site=sites.id WHERE
  231. subscriptions.id=?"""
  232. result_cursor = self._execute_statement(sub_query, (sub_id,))
  233. result = result_cursor.fetchall()
  234. return result
  235. def get_subscription_title(self, sub_id):
  236. """Retrieves only the title of a specified subscription from the
  237. database
  238. """
  239. title_query = "SELECT title from subscriptions where id=?"
  240. result_cursor = self._execute_statement(title_query, (sub_id,))
  241. result = result_cursor.fetchall()
  242. return result
  243. def mark_video_downloaded(self, video_id):
  244. """ Marks all videos in a specified subscription as downloaded"""
  245. update_statement = "UPDATE videos SET downloaded = 1 " \
  246. "WHERE subscription_id =?"
  247. self._execute_statement(update_statement, (video_id,))
  248. def get_subscriptions(self):
  249. """Retrieves all subscriptions from the database"""
  250. subscriptions_list = []
  251. subscriptions_query = """SELECT subscriptions.id, subscriptions.title,
  252. type, name,searchstring, directory,disabled, sites.title FROM
  253. subscriptions INNER JOIN sites ON subscriptions.site=sites.id;"""
  254. result_cursor = self._execute_statement(subscriptions_query)
  255. result = result_cursor.fetchall()
  256. for sub in result:
  257. subscriptions_list.append(
  258. subscription.Sub(subscription_type=sub[2], name=sub[3],
  259. site=sub[7], search=sub[4],
  260. subscription_id=sub[0], title=sub[1],
  261. directory=sub[5], disabled=sub[6]))
  262. return subscriptions_list
  263. def vacuum(self):
  264. """Vacuums the database, shrinking it in size"""
  265. self._execute_statement("VACUUM")
  266. def change_subscription_state(self, sub_id, state):
  267. """Enables or disables a subscription depending on the parameter
  268. state
  269. """
  270. update_statement = "UPDATE subscriptions SET disabled=? WHERE id=?"
  271. self._execute_statement(update_statement, (state, sub_id))
  272. def add_site(self, name):
  273. """
  274. Adds a site with the specified name to the database.
  275. :param name: name of the new site
  276. :type name: str
  277. """
  278. insert_statement = "INSERT INTO sites (title) VALUES (?)"
  279. self._execute_statement(insert_statement, (name,))
  280. def remove_site(self, name):
  281. """
  282. Removes a site with the specified name to the database.
  283. :param name: name of the new site
  284. :type name: str
  285. """
  286. site_id = self.get_site_id(name)
  287. subscriptions = self._get_subscriptions_by_site_id(site_id)
  288. delete_videos = "DELETE FROM videos WHERE subscription_id = ?"
  289. delete_subscription = "DELETE FROM SUBSCRIPTIONS WHERE id=?"
  290. for sub in subscriptions:
  291. self._execute_statement(delete_videos, (sub,))
  292. self._execute_statement(delete_subscription, (sub,))
  293. delete_site = "DELETE FROM sites WHERE id=?"
  294. self._execute_statement(delete_site, (site_id,))
  295. def get_site_id(self, name):
  296. """
  297. Get the ID of a specific site
  298. :param name: name of the new site
  299. :type name: str
  300. :return: the site ID
  301. :rtype: int
  302. """
  303. query = "SELECT id FROM sites WHERE title=?"
  304. cursor = self._execute_statement(query, (name,))
  305. result = cursor.fetchone()[0]
  306. return result
  307. def get_sites(self):
  308. """
  309. Retrieves all sites from the database.
  310. :return: list of sites with their respective IDs
  311. :rtype: tuple
  312. """
  313. query = "SELECT id,title FROM sites"
  314. cursor = self._execute_statement(query)
  315. result = cursor.fetchall()
  316. return result
  317. def _get_subscriptions_by_site_id(self, site_id):
  318. """
  319. Retrieves all subscriptions associated with the specified site_id
  320. from the database.
  321. :param site_id: ID of the site
  322. :type site_id: int
  323. :return: list of subscriptions associated with the site_id
  324. :rtype: tuple
  325. """
  326. query = "SELECT id FROM subscriptions WHERE site=?"
  327. cursor = self._execute_statement(query, (site_id,))
  328. return cursor.fetchall()