Scripting way to upload videos to peertube and youtube
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

454 lines
19 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. """
  4. prismedia - tool to upload videos to Peertube and Youtube
  5. Usage:
  6. prismedia --file=<FILE> [options]
  7. prismedia -f <FILE> --tags=STRING [options]
  8. prismedia --hearthbeat
  9. prismedia -h | --help
  10. prismedia --version
  11. Options:
  12. -f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option.
  13. --name=NAME Name of the video to upload. (default to video filename)
  14. -d, --description=STRING Description of the video. (default: default description)
  15. -t, --tags=STRING Tags for the video. comma separated.
  16. WARN: tags with punctuation (!, ', ", ?, ...)
  17. are not supported by Mastodon to be published from Peertube
  18. -c, --category=STRING Category for the videos, see below. (default: Films)
  19. --cca License should be CreativeCommon Attribution (affects Youtube upload only)
  20. -p, --privacy=STRING Choose between public, unlisted or private. (default: private)
  21. --disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
  22. --nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
  23. --nfo=STRING Configure a specific nfo file to set options for the video.
  24. By default Prismedia search a .txt based on the video name and will
  25. decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded)
  26. See nfo_example.txt for more details
  27. --platform=STRING List of platform(s) to upload to, comma separated.
  28. Supported platforms are youtube and peertube (default is both)
  29. --language=STRING Specify the default language for video. See below for supported language. (default is English)
  30. --publishAt=DATE Publish the video at the given DATE using local server timezone.
  31. DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
  32. DATE should be in the future
  33. --peertubeAt=DATE
  34. --youtubeAt=DATE Override publishAt for the corresponding platform. Allow to create preview on specific platform
  35. --originalDate=DATE Configure the video as initially recorded at DATE
  36. DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
  37. DATE should be in the past
  38. --auto-originalDate Automatically use the file modification time as original date
  39. --thumbnail=STRING Path to a file to use as a thumbnail for the video.
  40. Supported types are jpg and jpeg.
  41. By default, prismedia search for an image based on video name followed by .jpg or .jpeg
  42. --channel=STRING Set the channel to use for the video (Peertube only)
  43. If the channel is not found, spawn an error except if --channelCreate is set.
  44. --channelCreate Create the channel if not exists. (Peertube only, default do not create)
  45. Only relevant if --channel is set.
  46. --playlist=STRING Set the playlist to use for the video.
  47. If the playlist is not found, spawn an error except if --playlistCreate is set.
  48. --playlistCreate Create the playlist if not exists. (default do not create)
  49. Only relevant if --playlist is set.
  50. --progress=STRING Set the progress bar view, one of percentage, bigFile (MB), accurate (KB).
  51. --hearthbeat Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently)
  52. -h --help Show this help.
  53. --version Show version.
  54. Logging options
  55. -q --quiet Suppress any log except Critical (alias for --log=critical).
  56. --log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info)
  57. -u --url-only Display generated URL after upload directly on stdout, implies --quiet
  58. --batch Display generated URL after upload with platform information for easier parsing. Implies --quiet
  59. Be careful --batch and --url-only are mutually exclusives.
  60. --debug (Deprecated) Alias for --log=debug. Ignored if --log is set
  61. Strict options:
  62. Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
  63. forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
  64. tags, thumbnail, ...
  65. All strict option are optionals and are provided only to avoid errors when uploading :-)
  66. All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
  67. All strict options are off by default
  68. --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory
  69. --withThumbnail Prevent the upload without a thumbnail
  70. --withName Prevent the upload if no name are found
  71. --withDescription Prevent the upload without description
  72. --withTags Prevent the upload without tags
  73. --withPlaylist Prevent the upload if no playlist
  74. --withPublishAt Prevent the upload if no schedule
  75. --withOriginalDate Prevent the upload if no original date configured
  76. --withPlatform Prevent the upload if at least one platform is not specified
  77. --withCategory Prevent the upload if no category
  78. --withLanguage Prevent upload if no language
  79. --withChannel Prevent upload if no channel
  80. Categories:
  81. Category is the type of video you upload. Default is films.
  82. Here are available categories from Peertube and Youtube:
  83. music, films, vehicles,
  84. sports, travels, gaming, people,
  85. comedy, entertainment, news,
  86. how to, education, activism, science & technology,
  87. science, technology, animals
  88. Languages:
  89. Language of the video (audio track), choose one. Default is English
  90. Here are available languages from Peertube and Youtube:
  91. Arabic, English, French, German, Hindi, Italian,
  92. Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
  93. """
  94. import sys
  95. if sys.version_info[0] < 3:
  96. raise Exception("Python 3 or a more recent version is required.")
  97. import os
  98. import datetime
  99. import logging
  100. logger = logging.getLogger('Prismedia')
  101. from docopt import docopt
  102. from . import yt_upload
  103. from . import pt_upload
  104. from . import utils
  105. try:
  106. # noinspection PyUnresolvedReferences
  107. from schema import Schema, And, Or, Optional, SchemaError, Hook, Use
  108. except ImportError:
  109. logger.critical('This program requires that the `schema` data-validation library'
  110. ' is installed: \n'
  111. 'see https://github.com/halst/schema\n')
  112. exit(1)
  113. try:
  114. # noinspection PyUnresolvedReferences
  115. import magic
  116. except ImportError:
  117. logger.critical('This program requires that the `python-magic` library'
  118. ' is installed, NOT the Python bindings to libmagic API \n'
  119. 'see https://github.com/ahupp/python-magic\n')
  120. exit(1)
  121. VERSION = "prismedia v0.11.0"
  122. VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
  123. VALID_CATEGORIES = (
  124. "music", "films", "vehicles",
  125. "sports", "travels", "gaming", "people",
  126. "comedy", "entertainment", "news",
  127. "how to", "education", "activism", "science & technology",
  128. "science", "technology", "animals"
  129. )
  130. VALID_PLATFORM = ('youtube', 'peertube', 'none')
  131. VALID_LANGUAGES = ('arabic', 'english', 'french',
  132. 'german', 'hindi', 'italian',
  133. 'japanese', 'korean', 'mandarin',
  134. 'portuguese', 'punjabi', 'russian', 'spanish')
  135. VALID_PROGRESS = ('percentage', 'bigfile', 'accurate')
  136. def validateVideo(path):
  137. supported_types = ['video/mp4']
  138. detected_type = magic.from_file(path, mime=True)
  139. if detected_type not in supported_types:
  140. print("File", path, "detected type is", detected_type, "which is not one of", supported_types)
  141. force_file = ['y', 'yes']
  142. is_forcing = input("Are you sure you selected the correct file? (y/N)")
  143. if is_forcing.lower() not in force_file:
  144. return False
  145. return path
  146. def validateCategory(category):
  147. if category.lower() in VALID_CATEGORIES:
  148. return True
  149. else:
  150. return False
  151. def validatePrivacy(privacy):
  152. if privacy.lower() in VALID_PRIVACY_STATUSES:
  153. return True
  154. else:
  155. return False
  156. def validatePlatform(platform):
  157. for plfrm in platform.split(','):
  158. if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
  159. return False
  160. return True
  161. def validateLanguage(language):
  162. if language.lower() in VALID_LANGUAGES:
  163. return True
  164. else:
  165. return False
  166. def validatePublishDate(publishDate):
  167. # Check date format and if date is future
  168. try:
  169. now = datetime.datetime.now()
  170. publishAt = datetime.datetime.strptime(publishDate, '%Y-%m-%dT%H:%M:%S')
  171. if now >= publishAt:
  172. return False
  173. except ValueError:
  174. return False
  175. return True
  176. def validateOriginalDate(originalDate):
  177. # Check date format and if date is past
  178. try:
  179. now = datetime.datetime.now()
  180. originalDate = datetime.datetime.strptime(originalDate, '%Y-%m-%dT%H:%M:%S')
  181. if now <= originalDate:
  182. return False
  183. except ValueError:
  184. return False
  185. return True
  186. def validateThumbnail(thumbnail):
  187. supported_types = ['image/jpg', 'image/jpeg']
  188. if os.path.exists(thumbnail) and \
  189. magic.from_file(thumbnail, mime=True) in supported_types:
  190. return thumbnail
  191. else:
  192. return False
  193. def validateLogLevel(loglevel):
  194. numeric_level = getattr(logging, loglevel, None)
  195. if not isinstance(numeric_level, int):
  196. return False
  197. return True
  198. def validateProgress(progress):
  199. for prgs in progress.split(','):
  200. if prgs.lower().replace(" ", "") not in VALID_PROGRESS:
  201. return False
  202. return True
  203. def _optionnalOrStrict(key, scope, error):
  204. option = key.replace('-', '')
  205. option = option[0].upper() + option[1:]
  206. if scope["--with" + option] is True and scope[key] is None:
  207. logger.critical("Prismedia: you have required the strict presence of " + key + " but none is found")
  208. exit(1)
  209. return True
  210. def configureLogs(options):
  211. if options.get('--batch') and options.get('--url-only'):
  212. logger.critical("Prismedia: Please use either --batch OR --url-only, not both.")
  213. exit(1)
  214. # batch and url-only implies quiet
  215. if options.get('--batch') or options.get('--url-only'):
  216. options['--quiet'] = True
  217. if options.get('--quiet'):
  218. # We need to set both log level in the same time
  219. logger.setLevel(50)
  220. ch.setLevel(50)
  221. elif options.get('--log'):
  222. numeric_level = getattr(logging, options["--log"], None)
  223. # We need to set both log level in the same time
  224. logger.setLevel(numeric_level)
  225. ch.setLevel(numeric_level)
  226. elif options.get('--debug'):
  227. logger.warning("DEPRECATION: --debug is deprecated, please use --log=debug instead")
  228. logger.setLevel(10)
  229. ch.setLevel(10)
  230. def configureStdoutLogs():
  231. logger_stdout = logging.getLogger('stdoutlogs')
  232. logger_stdout.setLevel(logging.INFO)
  233. ch_stdout = logging.StreamHandler(stream=sys.stdout)
  234. ch_stdout.setLevel(logging.INFO)
  235. # Default stdout logs is url only
  236. formatter_stdout = logging.Formatter('%(message)s')
  237. ch_stdout.setFormatter(formatter_stdout)
  238. logger_stdout.addHandler(ch_stdout)
  239. def main():
  240. options = docopt(__doc__, version=VERSION)
  241. earlyoptionSchema = Schema({
  242. Optional('--log'): Or(None, And(
  243. str,
  244. Use(str.upper),
  245. validateLogLevel,
  246. error="Log level not recognized")
  247. ),
  248. Optional('--quiet', default=False): bool,
  249. Optional('--debug'): bool,
  250. Optional('--url-only', default=False): bool,
  251. Optional('--batch', default=False): bool,
  252. Optional('--withNFO', default=False): bool,
  253. Optional('--withThumbnail', default=False): bool,
  254. Optional('--withName', default=False): bool,
  255. Optional('--withDescription', default=False): bool,
  256. Optional('--withTags', default=False): bool,
  257. Optional('--withPlaylist', default=False): bool,
  258. Optional('--withPublishAt', default=False): bool,
  259. Optional('--withOriginalDate', default=False): bool,
  260. Optional('--withPlatform', default=False): bool,
  261. Optional('--withCategory', default=False): bool,
  262. Optional('--withLanguage', default=False): bool,
  263. Optional('--withChannel', default=False): bool,
  264. # This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
  265. object: object
  266. })
  267. schema = Schema({
  268. '--file': And(str, os.path.exists, validateVideo, error='file is not supported, please use mp4'),
  269. # Strict option checks - at the moment Schema needs to check Hook and Optional separately #
  270. Hook('--name', handler=_optionnalOrStrict): object,
  271. Hook('--description', handler=_optionnalOrStrict): object,
  272. Hook('--tags', handler=_optionnalOrStrict): object,
  273. Hook('--category', handler=_optionnalOrStrict): object,
  274. Hook('--language', handler=_optionnalOrStrict): object,
  275. Hook('--platform', handler=_optionnalOrStrict): object,
  276. Hook('--publishAt', handler=_optionnalOrStrict): object,
  277. Hook('--originalDate', handler=_optionnalOrStrict): object,
  278. Hook('--thumbnail', handler=_optionnalOrStrict): object,
  279. Hook('--channel', handler=_optionnalOrStrict): object,
  280. Hook('--playlist', handler=_optionnalOrStrict): object,
  281. # Validate checks #
  282. Optional('--name'): Or(None, And(
  283. str,
  284. lambda x: not x.isdigit(),
  285. error="The video name should be a string")
  286. ),
  287. Optional('--description'): Or(None, And(
  288. str,
  289. lambda x: not x.isdigit(),
  290. error="The video description should be a string")
  291. ),
  292. Optional('--tags'): Or(None, And(
  293. str,
  294. lambda x: not x.isdigit(),
  295. error="Tags should be a string")
  296. ),
  297. Optional('--category'): Or(None, And(
  298. str,
  299. validateCategory,
  300. error="Category not recognized, please see --help")
  301. ),
  302. Optional('--language'): Or(None, And(
  303. str,
  304. validateLanguage,
  305. error="Language not recognized, please see --help")
  306. ),
  307. Optional('--privacy'): Or(None, And(
  308. str,
  309. validatePrivacy,
  310. error="Please use recognized privacy between public, unlisted or private")
  311. ),
  312. Optional('--nfo'): Or(None, str),
  313. Optional('--platform'): Or(None, And(str, validatePlatform, error="Sorry, upload platform not supported")),
  314. Optional('--publishAt'): Or(None, And(
  315. str,
  316. validatePublishDate,
  317. error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
  318. ),
  319. Optional('--peertubeAt'): Or(None, And(
  320. str,
  321. validatePublishDate,
  322. error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
  323. ),
  324. Optional('--youtubeAt'): Or(None, And(
  325. str,
  326. validatePublishDate,
  327. error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
  328. ),
  329. Optional('--originalDate'): Or(None, And(
  330. str,
  331. validateOriginalDate,
  332. error="Original date should be the form YYYY-MM-DDThh:mm:ss and has to be in the past")
  333. ),
  334. Optional('--auto-originalDate'): bool,
  335. Optional('--cca'): bool,
  336. Optional('--disable-comments'): bool,
  337. Optional('--nsfw'): bool,
  338. Optional('--thumbnail'): Or(None, And(
  339. str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'),
  340. ),
  341. Optional('--channel'): Or(None, str),
  342. Optional('--channelCreate'): bool,
  343. Optional('--playlist'): Or(None, str),
  344. Optional('--playlistCreate'): bool,
  345. Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
  346. '--hearthbeat': bool,
  347. '--help': bool,
  348. '--version': bool,
  349. # This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
  350. object: object
  351. })
  352. if options.get('--hearthbeat'):
  353. yt_upload.hearthbeat()
  354. exit(0)
  355. # We need to validate early options first as withNFO and logs options should be prioritized
  356. try:
  357. options = earlyoptionSchema.validate(options)
  358. configureLogs(options)
  359. except SchemaError as e:
  360. logger.critical(e)
  361. exit(1)
  362. if options.get('--url-only') or options.get('--batch'):
  363. configureStdoutLogs()
  364. options = utils.parseNFO(options)
  365. # If after loading NFO we still has no original date and --auto-originalDate is enabled,
  366. # then we need to search from the file
  367. # We need to do that before the strict validation in case --withOriginalDate is enabled
  368. if not options.get('--originalDate') and options.get('--auto-originalDate'):
  369. options['--originalDate'] = utils.searchOriginalDate(options)
  370. # Once NFO are loaded, we need to revalidate strict options in case some were in NFO
  371. try:
  372. options = earlyoptionSchema.validate(options)
  373. except SchemaError as e:
  374. logger.critical(e)
  375. exit(1)
  376. if not options.get('--thumbnail'):
  377. options = utils.searchThumbnail(options)
  378. try:
  379. options = schema.validate(options)
  380. except SchemaError as e:
  381. logger.critical(e)
  382. exit(1)
  383. logger.debug("Python " + sys.version)
  384. logger.debug(options)
  385. if options.get('--platform') is None or "peertube" in options.get('--platform'):
  386. pt_upload.run(options)
  387. if options.get('--platform') is None or "youtube" in options.get('--platform'):
  388. yt_upload.run(options)
  389. if __name__ == '__main__':
  390. logger.warning("DEPRECATION: use 'python -m prismedia', not 'python -m prismedia.upload'")
  391. main()