program.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. # This file is part of stov, written by Helmut Pozimski 2012-2017.
  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 contains the functions that make up the core of the
  16. application.
  17. """
  18. import logging
  19. import sys
  20. import smtplib
  21. import socket
  22. from email.mime.text import MIMEText
  23. from email.mime.multipart import MIMEMultipart
  24. from lib_stov import subscription
  25. from lib_stov import stov_exceptions
  26. LOGGER = logging.getLogger("stov")
  27. def add_subscription(conf, database, channel="",
  28. search="", playlist="", site="youtube"):
  29. """
  30. Takes care of adding a new subscription to the database.
  31. :param conf: configuration object
  32. :type conf: lib_stov.configuration.Conf
  33. :param database: database object
  34. :type database: lib_stov.database.Db
  35. :param site: site the subscription is about to be created for
  36. :type site: str
  37. :param channel: optional channel name
  38. :type channel: str
  39. :param search: optional search string
  40. :type search: str
  41. :param playlist: optional playlist ID
  42. :type playlist: str
  43. """
  44. LOGGER.debug(_("Creating new subscription with the following "
  45. "parameters:\nChannel: %s\nSearch: %s\nPlaylist: %s"),
  46. channel, search, playlist)
  47. try:
  48. if channel and not search:
  49. new_subscription = subscription.Sub(subscription_type="user",
  50. name=channel, conf=conf,
  51. site=site)
  52. elif channel and search:
  53. new_subscription = subscription.Sub(subscription_type="user",
  54. name=channel,
  55. search=search,
  56. conf=conf, site=site)
  57. elif not channel and search:
  58. new_subscription = subscription.Sub(subscription_type="search",
  59. name=_("Search_"),
  60. search=search,
  61. conf=conf, site=site)
  62. elif playlist:
  63. if search:
  64. LOGGER.error(_("Playlists do not support searching, the "
  65. "search option will be ignored!"))
  66. new_subscription = subscription.Sub(subscription_type="playlist",
  67. name=playlist,
  68. conf=conf, site=site)
  69. else:
  70. LOGGER.error(_("None or invalid subscription type given, please "
  71. "check the type option and try again."))
  72. sys.exit(1)
  73. except stov_exceptions.TypeNotSupported as error:
  74. LOGGER.error(error)
  75. sys.exit(1)
  76. try:
  77. subscription_data = new_subscription.add_sub()
  78. site_id = database.get_site_id(subscription_data[6])
  79. new_sub_data = (subscription_data[0], subscription_data[1],
  80. subscription_data[2], subscription_data[3],
  81. subscription_data[4], subscription_data[5],
  82. site_id)
  83. subscription_id = database.insert_subscription(new_sub_data)
  84. new_subscription.set_id(subscription_id)
  85. except stov_exceptions.DBWriteAccessFailedException as error:
  86. LOGGER.error(error)
  87. sys.exit(1)
  88. except stov_exceptions.ChannelNotFound as error:
  89. LOGGER.error(error)
  90. sys.exit(1)
  91. else:
  92. LOGGER.debug(_("Subscription sucessfully inserted into database."))
  93. try:
  94. new_subscription.update_data()
  95. except stov_exceptions.YoutubeAPITimeoutException as error:
  96. LOGGER.error(error)
  97. except stov_exceptions.NoDataFromYoutubeAPIException as error:
  98. LOGGER.error(error)
  99. for video in new_subscription.parsed_response.videos:
  100. if not database.video_in_database(video.video_id):
  101. if new_subscription.check_string_match(video):
  102. try:
  103. database.insert_video(video, new_subscription.get_id())
  104. except stov_exceptions.DBWriteAccessFailedException as error:
  105. LOGGER.error(error)
  106. sys.exit(1)
  107. else:
  108. LOGGER.debug(_("Video %s successfully inserted into "
  109. "database."), video.title)
  110. LOGGER.info(_("New subscription ") + new_subscription.get_title() +
  111. _(" successfully added"))
  112. def list_subscriptions(conf, database):
  113. """
  114. Prints a list of subscriptions from the database.
  115. :param conf: configuration object
  116. :type conf: lib_stov.configuration.Conf
  117. :param database: database object
  118. :type database: lib_stov.database.Db
  119. """
  120. subscriptions_list = database.get_subscriptions(conf)
  121. sub_state = None
  122. if subscriptions_list:
  123. LOGGER.info(_("ID Title Site"))
  124. for sub in subscriptions_list:
  125. if not sub.disabled:
  126. sub_state = _("enabled")
  127. elif sub.disabled:
  128. sub_state = _("disabled")
  129. LOGGER.info(str(sub.get_id()) + " " + sub.get_title() +
  130. " " + sub.site + " " + "(%s)" % sub_state)
  131. else:
  132. LOGGER.info(_("No subscriptions added yet, add one!"))
  133. def delete_subscription(database, sub_id):
  134. """
  135. Deletes a specified subscription from the database
  136. :param database: database object
  137. :type database: lib_stov.database.Db
  138. :param sub_id: ID of the subscription to be deleted
  139. :type sub_id: int
  140. """
  141. try:
  142. database.delete_subscription(sub_id)
  143. except stov_exceptions.SubscriptionNotFoundException as error:
  144. LOGGER.error(error)
  145. sys.exit(1)
  146. except stov_exceptions.DBWriteAccessFailedException as error:
  147. LOGGER.error(error)
  148. sys.exit(1)
  149. else:
  150. LOGGER.info(_("Subscription deleted successfully!"))
  151. def update_subscriptions(database, conf, subscriptions=None):
  152. """
  153. Updates data about videos in a subscription.
  154. :param conf: configuration object
  155. :type conf: lib_stov.configuration.Conf
  156. :param database: database object
  157. :type database: lib_stov.database.Db
  158. :param subscriptions: list of subscriptions to update
  159. :type subscriptions: list
  160. """
  161. subscriptions_list = get_subscriptions(conf, database, subscriptions)
  162. for element in subscriptions_list:
  163. LOGGER.debug(_("Updating subscription %s"), element.get_title())
  164. videos = database.get_videos(element.get_id(), conf)
  165. element.gather_videos(videos)
  166. try:
  167. element.update_data()
  168. except stov_exceptions.YoutubeAPITimeoutException as error:
  169. LOGGER.error(error)
  170. except stov_exceptions.NoDataFromYoutubeAPIException as error:
  171. LOGGER.error(error)
  172. for video in element.parsed_response.videos:
  173. if not database.video_in_database(video.video_id):
  174. if element.check_string_match(video):
  175. try:
  176. database.insert_video(video, element.get_id())
  177. except stov_exceptions.DBWriteAccessFailedException as \
  178. error:
  179. LOGGER.error(error)
  180. sys.exit(1)
  181. else:
  182. LOGGER.debug(_("Video %s successfully inserted into "
  183. "database."), video.title)
  184. def download_videos(database, conf, subscriptions=None):
  185. """
  186. Downloads videos that haven't been previously downloaded.
  187. :param conf: configuration object
  188. :type conf: lib_stov.configuration.Conf
  189. :param database: database object
  190. :type database: lib_stov.database.Db
  191. :param subscriptions: list of subscriptions to consider for downloading
  192. :type subscriptions: list
  193. :return: tuple containing (in that order) downloaded videos, failed \
  194. videos and a list of the videos downloaded
  195. :rtype: tuple
  196. """
  197. video_titles = []
  198. subscriptions_list = get_subscriptions(conf, database, subscriptions)
  199. videos_downloaded = 0
  200. videos_failed = 0
  201. for sub in subscriptions_list:
  202. videos = database.get_videos(sub.get_id(), conf)
  203. sub.gather_videos(videos)
  204. try:
  205. sub.download_videos()
  206. except stov_exceptions.SubscriptionDisabledException as error:
  207. LOGGER.debug(error)
  208. for entry in sub.downloaded_videos:
  209. database.update_video_download_status(entry.get_id(), 1)
  210. video_titles.append(entry.title)
  211. videos_downloaded = len(video_titles)
  212. videos_failed = videos_failed + sub.failed_videos_count
  213. for video in sub.failed_videos:
  214. try:
  215. database.update_video_fail_count(video.failcnt, video.get_id())
  216. if video.failcnt >= int(conf.values["maxfails"]):
  217. database.disable_failed_video(video.get_id())
  218. except stov_exceptions.DBWriteAccessFailedException as error:
  219. LOGGER.error(error)
  220. sys.exit(1)
  221. return (videos_downloaded, videos_failed, video_titles)
  222. def compose_email(conf, downloaded_videos, video_titles):
  223. """
  224. Composes an e-mail that can be send out to the user.
  225. :param conf: configuration object
  226. :type conf: lib_stov.configuration.Conf
  227. :param downloaded_videos: number of downloaded videos
  228. :type downloaded_videos: int
  229. :param video_titles: titles of the downloaded videos
  230. :type video_titles: list
  231. :return: e-mail contents
  232. :rtype: MIMEMultipart
  233. """
  234. mail_text = ""
  235. msg = MIMEMultipart()
  236. if downloaded_videos == 1:
  237. msg["Subject"] = _("Downloaded %i new video") % downloaded_videos
  238. mail_text = _("The following episode has been downloaded by stov: "
  239. "\n\n")
  240. else:
  241. msg["Subject"] = _("Downloaded %i new videos") % downloaded_videos
  242. mail_text = _("The following episodes have been downloaded by "
  243. "stov: \n\n")
  244. msg["From"] = "stov <%s>" % conf.values["mailfrom"]
  245. msg["To"] = "<%s>" % conf.values["mailto"]
  246. for line in video_titles:
  247. mail_text += line + "\n"
  248. msg_text = MIMEText(mail_text.encode("utf8"), _charset="utf8")
  249. msg.attach(msg_text)
  250. return msg
  251. def send_email(conf, msg):
  252. """
  253. Sends an e-mail to the user.
  254. :param conf: configuration object
  255. :type conf: lib_stov.configuration.Conf
  256. :param msg: message to be sent
  257. :type msg: MIMEMultipart
  258. """
  259. server_connection = smtplib.SMTP(conf.values["mailhost"],
  260. conf.values["smtpport"])
  261. try:
  262. server_connection.connect()
  263. except (smtplib.SMTPConnectError, smtplib.SMTPServerDisconnected,
  264. socket.error):
  265. LOGGER.error(_("Could not connect to the smtp server, please check"
  266. " your settings!"))
  267. else:
  268. try:
  269. server_connection.starttls()
  270. except smtplib.SMTPException:
  271. LOGGER.debug(_("TLS not available, proceeding unencrypted."))
  272. if conf.values["auth_needed"] == "yes":
  273. try:
  274. server_connection.login(conf.values["user_name"],
  275. conf.values["password"])
  276. except smtplib.SMTPAuthenticationError:
  277. LOGGER.error(_("Authentication failed, please check user "
  278. "name and password!"))
  279. except smtplib.SMTPException:
  280. LOGGER.error(_("Could not authenticate, server probably "
  281. "does not support authentication!"))
  282. try:
  283. server_connection.sendmail(conf.values["mailfrom"],
  284. conf.values["mailto"],
  285. msg.as_string())
  286. except smtplib.SMTPRecipientsRefused:
  287. LOGGER.error(_("The server refused the recipient address, "
  288. "please check your settings."))
  289. except smtplib.SMTPSenderRefused:
  290. LOGGER.error(_("The server refused the sender address, "
  291. "please check your settings."))
  292. server_connection.quit()
  293. def list_videos(database, conf, sub_id):
  294. """
  295. Lists all videos in a specified subscription
  296. :param database: database object
  297. :type database: lib_stov.database.Db
  298. :param conf: configuration object
  299. :type conf: lib_stov.configuration.Conf
  300. :param sub_id: ID of the subscription
  301. :type sub_id: int
  302. """
  303. try:
  304. data = database.get_subscription(sub_id)
  305. except stov_exceptions.DBWriteAccessFailedException as error:
  306. LOGGER.error(error)
  307. sys.exit(1)
  308. else:
  309. if data:
  310. sub = subscription.Sub(subscription_id=data[0][0],
  311. title=data[0][1],
  312. subscription_type=data[0][2],
  313. name=data[0][3],
  314. search=data[0][4],
  315. directory=data[0][5],
  316. disabled=data[0][6],
  317. site=data[0][7], conf=conf)
  318. videos = database.get_videos(sub.get_id(), conf)
  319. sub.gather_videos(videos)
  320. videos_list = sub.print_videos()
  321. for video in videos_list:
  322. LOGGER.info(video)
  323. else:
  324. LOGGER.error(_("Invalid subscription, please check the list and "
  325. "try again."))
  326. def catchup(database, sub_id):
  327. """
  328. Marks all videos in a subscription as downloaded
  329. :param database: database object
  330. :type database: lib_stov.database.Db
  331. :param sub_id: ID of the subscription
  332. :type sub_id: int
  333. """
  334. try:
  335. sub_data = database.get_subscription_title(sub_id)
  336. except stov_exceptions.DBWriteAccessFailedException as error:
  337. LOGGER.error(error)
  338. sys.exit(1)
  339. else:
  340. if sub_data:
  341. try:
  342. database.mark_video_downloaded(sub_id)
  343. except stov_exceptions.DBWriteAccessFailedException as error:
  344. LOGGER.error(error)
  345. else:
  346. LOGGER.error(_("The subscription could not be updated, "
  347. "please check if the ID given is correct."))
  348. def clean_database(database, conf):
  349. """
  350. Initiates a database cleanup, deleting all videos that are no longer
  351. in the scope of the query and vacuuming the database to free up space.
  352. :param database: database object
  353. :type database: lib_stov.database.Db
  354. :param conf: configuration object
  355. :type conf: lib_stov.configuration.Conf
  356. """
  357. subscription_list = database.get_subscriptions(conf)
  358. for element in subscription_list:
  359. videos = database.get_videos(element.get_id(), conf)
  360. element.check_and_delete(videos)
  361. for delete_video in element.to_delete:
  362. LOGGER.debug(_("Deleting video %s from "
  363. "database"), delete_video.title)
  364. try:
  365. database.delete_video(delete_video.get_id())
  366. except stov_exceptions.DBWriteAccessFailedException as error:
  367. LOGGER.error(error)
  368. sys.exit(1)
  369. try:
  370. database.vacuum()
  371. except stov_exceptions.DBWriteAccessFailedException as error:
  372. LOGGER.error(error)
  373. sys.exit(1)
  374. def change_subscription_state(database, sub_id, enable=False):
  375. """
  376. Enables or disables a subscription.
  377. :param database: database object
  378. :type database: lib_stov.database.Db
  379. :param sub_id: ID of the subscription
  380. :type sub_id: int
  381. :param enable: whether to enable or disable the subscription
  382. :type enable: bool
  383. """
  384. subscription_state = database.get_subscription(sub_id)
  385. try:
  386. if enable:
  387. if int(subscription_state[0][6]) == 0:
  388. LOGGER.error(_("The subscription ID %s is already enabled."),
  389. sub_id)
  390. elif int(subscription_state[0][6]) == 1:
  391. try:
  392. database.change_subscription_state(sub_id, 0)
  393. except stov_exceptions.DBWriteAccessFailedException as error:
  394. LOGGER.error(error)
  395. sys.exit(1)
  396. else:
  397. LOGGER.info(_("Enabled subscription ID %s."), sub_id)
  398. else:
  399. if int(subscription_state[0][6]) == 1:
  400. LOGGER.error(_("Subscription ID %s is already disabled."),
  401. sub_id)
  402. elif int(subscription_state[0][6]) == 0:
  403. try:
  404. database.change_subscription_state(sub_id, 1)
  405. except stov_exceptions.DBWriteAccessFailedException as error:
  406. LOGGER.error(error)
  407. sys.exit(1)
  408. else:
  409. LOGGER.info(_("Disabled subscription ID %s."),
  410. sub_id)
  411. except IndexError:
  412. LOGGER.error(_("Could not find the subscription with ID %s, "
  413. "please check and try again."), sub_id)
  414. def print_license():
  415. """
  416. Prints the license of the application.
  417. """
  418. LOGGER.info("""
  419. stov is free software: you can redistribute it and/or modify
  420. it under the terms of the GNU General Public License as published by
  421. the Free Software Foundation, version 2 of the License.
  422. stov is distributed in the hope that it will be useful,
  423. but WITHOUT ANY WARRANTY; without even the implied warranty of
  424. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  425. GNU General Public License for more details.
  426. You should have received a copy of the GNU General Public License
  427. along with stov. If not, see <http://www.gnu.org/licenses/>.""")
  428. def download_notify(database, conf, subscriptions=None):
  429. """
  430. starts an update of not yet downloaded videos and notifies the user
  431. :param database: database object
  432. :type database: lib_stov.database.Db
  433. :param conf: configuration object
  434. :type conf: lib_stov.configuration.Conf
  435. :param subscriptions: list of subscriptions to consider for downloading
  436. :type subscriptions: list
  437. """
  438. videos_downloaded, videos_failed, video_titles = \
  439. download_videos(database, conf, subscriptions)
  440. if videos_downloaded > 0 and conf.values["notify"] == "yes":
  441. msg = compose_email(conf, videos_downloaded, video_titles)
  442. send_email(conf, msg)
  443. elif videos_downloaded == 0 and videos_failed == 0:
  444. if conf.values["notify"] == "no":
  445. LOGGER.info(_("There are no videos to be downloaded."))
  446. elif conf.values["notify"] == "no":
  447. if videos_failed == 0:
  448. LOGGER.info(_("The following videos have been downloaded:\n"))
  449. for i in video_titles:
  450. LOGGER.info(i)
  451. else:
  452. if conf.values["notify"] != "yes":
  453. LOGGER.error(_("Could not determine how you want to be informed "
  454. "about new videos, please check the notify "
  455. "parameter in your configuration."))
  456. def initialize_sites(database):
  457. """
  458. Adds sites to the database if they are not in there yet.
  459. :param database: database object
  460. :type database: lib_stov.database.Db
  461. """
  462. supported_sites = ["youtube", "zdf_mediathek", "twitch", "vidme"]
  463. sites = database.get_sites()
  464. for site in supported_sites:
  465. site_found = False
  466. for result in sites:
  467. if site in result:
  468. site_found = True
  469. if not site_found:
  470. database.add_site(site)
  471. def list_sites(database):
  472. """
  473. Lists the currently supported sites.
  474. :param database: database object
  475. :type database: lib_stov.database.Db
  476. """
  477. sites = database.get_sites()
  478. LOGGER.info(_("Sites currently supported by stov:"))
  479. for entry in sites:
  480. LOGGER.info(entry[1])
  481. def get_subscriptions(conf, database, subscriptions=None):
  482. """
  483. Retrieves all or only specific subscriptions from the database and
  484. returns them as a list of subscription objects.
  485. :param conf: configuration object
  486. :type conf: lib_stov.configuration.Conf
  487. :param database: database object
  488. :type database: lib_stov.database.Db
  489. :param subscriptions: list of subscriptions to retrieve
  490. :type subscriptions: list
  491. :return: list of subscription objects
  492. :rtype: list
  493. """
  494. if subscriptions:
  495. subscriptions_list = []
  496. for element in subscriptions:
  497. data = database.get_subscription(element)
  498. if data:
  499. sub = subscription.Sub(subscription_id=data[0][0],
  500. title=data[0][1],
  501. subscription_type=data[0][2],
  502. name=data[0][3],
  503. search=data[0][4],
  504. directory=data[0][5],
  505. disabled=data[0][6],
  506. site=data[0][7], conf=conf)
  507. subscriptions_list.append(sub)
  508. else:
  509. LOGGER.error(
  510. _("Invalid subscription, please check the list and "
  511. "try again."))
  512. else:
  513. subscriptions_list = database.get_subscriptions(conf)
  514. return subscriptions_list