Compare commits

...

205 Commits

Author SHA1 Message Date
  Zykino b895a4543e Add progress for peertube plugin 2 years ago
  Zykino 46bd46455d WIP Add a configuration global object 2 years ago
  Zykino f90e084b67 WIP Rename and move a bunch of files 2 years ago
  Zykino a2c76bcbe1 WIP Fix typos and other recommendations from pycharm 2 years ago
  Zykino 819f7e04e2 WIP Finally add Yapsy as a dependency 2 years ago
  Zykino 67b8732c13 WIP add a quick debug plugin 2 years ago
  Zykino 311771a555 WIP make Peertube platform’s plugin upload videos. Light mode 2 years ago
  Zykino fe37d0be74 WIP Implements the core flow 3 years ago
  Zykino e7d55bc97e WIP Add a help plugin 3 years ago
  Zykino bcfc8e723a WIP Populate the Video object 3 years ago
  Zykino b5004a63ed WIP Add hearthbeat functionnality 3 years ago
  Zykino 85b86db751 WIP Use spaces for indentation 3 years ago
  Zykino 15edd7987b WIP Prepare the docstring 3 years ago
  Zykino 4f4842fd1f WIP Enumerates the plugins types instead of using magic strings 3 years ago
  Zykino c1e6a6e62d WIP Makes the plugins folder an modifiable if needed 3 years ago
  Zykino 979229aa01 WIP Include some command lines options 3 years ago
  Zykino cedce92dbe WIP Add a Video class to hold all of its properties 3 years ago
  Zykino c9a9b380ab WIP basis for integrating a plugin system 3 years ago
  LecygneNoir e0a63ed4b2 Revert README to use prismedia as it's indeed the binary script installed through pip 3 years ago
  LecygneNoir ba2a1ebb79 Merge branch 'feature/improve_genconfig' into develop 3 years ago
  LecygneNoir cf3d4c32c3 Better resilience for the genconfig function thanks to happy path and @Zykino suggestion! 3 years ago
  LecygneNoir 85f0fe9b6f Add ask_overwirte function to utils in order to be more general and usable in other modules 3 years ago
  LecygneNoir 1a006f3b6c Improve genconfig system and documentation for easier use, cf #55 3 years ago
  LecygneNoir cdef038323 Move logger initialization in __init__.py to be able to use it from any module 3 years ago
  LecygneNoir cbf3386bac Remove confusing warning when using genconfig, see #55 3 years ago
  LecygneNoir ca733e0dc3 Merge pull request 'hearthbeat (keepalive ?)' (#54) from Zykino/prismedia:hearthbeat into develop 3 years ago
  Zykino a4f162320d Fix some spacing formatting and documentation 3 years ago
  Zykino 29b1747c3e Visit all of Youtube’s playlists 3 years ago
  Zykino ea39fe9854 Fix some spacing 3 years ago
  Zykino a725e848ab Add an option to use some credits easiely 3 years ago
  LecygneNoir 9b6da1e3dc Merge tag 'v0.11.0' into develop 3 years ago
  LecygneNoir 194e2e4606 Merge branch 'release/v0.11.0' 3 years ago
  LecygneNoir 339caeb7f7 Bump files to v0.11.0, fix #50 3 years ago
  LecygneNoir e6375b5aa0 Merge pull request 'Add a progression bar for Peertube's upload' (#52) from Zykino/prismedia:feature/progression into develop 3 years ago
  Zykino 6add140732 Disable the progressbar when the user want a quiet output 3 years ago
  Zykino c4e3243131 Add clint as requirement 3 years ago
  Zykino 93f1205ab8 Make it possible to choose between multiples tipes of progress bar 3 years ago
  Zykino 09c2d84357 Add a progression bar to Peertube upload 3 years ago
  LecygneNoir 230ac545c4 Patch incorrect loading of NFO keys, breaking the match when the key contains -, and add example for auto-originalDate in NFO 3 years ago
  LecygneNoir 42ee7d761b Need to strip the getmtime timestamp as some OSes return timestamp with microsecond (XXXX.YYYY) 3 years ago
  LecygneNoir 1a937098d8 Merge branch 'feature/recording_date' into develop 3 years ago
  LecygneNoir 736582b495 Change the originalDate behaviour to not default, and add option to auto manage the original date if needed 3 years ago
  LecygneNoir 4a9fda5e77 Add one function to deal with date to avoid duplicate code 3 years ago
  LecygneNoir 8dc3a86aab Stripe the README from the full --help output to focus on some main features 3 years ago
  LecygneNoir 4b7c01a707 Add help for option originalDate and changelog about the feature 3 years ago
  LecygneNoir dc98f2e155 Add functions to manage Original Date of record for peertube 3 years ago
  LecygneNoir 60bf26418d Add the originalDate options to Youtube videos 3 years ago
  LecygneNoir 447310a17e Add options and bunch of functions to maange the originalDate fields in prismedia, cf #50 3 years ago
  LecygneNoir 9b597f461e Merge tag 'v0.10.3' into develop 3 years ago
  LecygneNoir 8b1470ab31 Merge branch 'hotfix/v0.10.3' 3 years ago
  LecygneNoir 6d15ad18ca Fix peertube pagination index for playlsit, as index begins at 0, not 1, shame on me! 😳 3 years ago
  LecygneNoir 5c991581e8 bump to v0.10.2 for poetry 3 years ago
  LecygneNoir 4956a19d0e bump to v0.10.2 for poetry 3 years ago
  LecygneNoir 2f8543b43c Merge tag 'v0.10.2' into develop 3 years ago
  LecygneNoir 5607c8ea06 Merge branch 'release/v0.10.2' 3 years ago
  LecygneNoir 25435453bd Bump to version v0.10.2 and fix typo in ticket number inside Changelog for v0.10.2 3 years ago
  LecygneNoir dab44244f3 Revert the workaround for Youtube playlist bug now the bug is fixed by Youtube (see #47) 3 years ago
  LecygneNoir 26476347d3 Renable Peertube upload after tests 😓 3 years ago
  LecygneNoir 5160e9e68d Add a check to avoid uploading on Peertube with more than 5 tags (see #48) 3 years ago
  LecygneNoir e61a70460d Merge branch 'feature/41_pt_playlistrecreated' into develop 3 years ago
  LecygneNoir 0aae4da68f Add changelog for #41 and #47 3 years ago
  LecygneNoir cbb7c745de Add pagination to find Peertube existing playlists as default pagination shows 14 playlists max. See #41 3 years ago
  LecygneNoir c2db597388 Fix message about thumbnail where it missed a space, close #49 3 years ago
  LecygneNoir e33a4c91a0 Merge tag 'v0.10.1' into develop 3 years ago
  LecygneNoir bb451e108d Merge branch 'hotfix/v0.10.1' 3 years ago
  LecygneNoir 28a7541fa8 Bump version to v0.10.1 3 years ago
  LecygneNoir 320c3b1a0b Fix bug introduced by v0.10.0 breaking thumbnail on youtube 3 years ago
  LecygneNoir fbfe3356ec Merge tag 'v0.10.0' into develop 3 years ago
  LecygneNoir e7a4d1656a Merge branch 'release/v0.10.0' 3 years ago
  LecygneNoir 1e9b719e0c Bump version to 0.10.0 and edit Changelog en Readme for the 0.10.0 3 years ago
  LecygneNoir ee578e8e82 Add a workaround Youtube API bug regarding playlist to ignore incorrect 404 return, see #47 for more details 3 years ago
  LecygneNoir f8ca4b093a Merge branch 'feature/logs_improvement' into develop 3 years ago
  LecygneNoir 379aef1dd8 Following discussion in #29, rename print-url option and add a batch options 3 years ago
  LecygneNoir 1c441bf67a Add new options --quiet and --print-url for an easier scripting use of prismedia, cf #29 3 years ago
  LecygneNoir dc7dd5cb46 Full rewrite of logging system to introduce --log as an option, and prepare the work for #29 3 years ago
  LecygneNoir 25682af83a Merge branch 'feature/strict_options' into develop 3 years ago
  LecygneNoir 542bb6f1f9 Correction of some typos 3 years ago
  LecygneNoir 5022aface4 Prepare changelog and documentation for strict check option 3 years ago
  LecygneNoir 003830696f Add strict options to force and check the existence of specific option before uploading, following issue #36. Need upgrade of Schema to have Hooks. 3 years ago
  LecygneNoir 2e4e876169 Merge tag 'v0.9.1' into develop 3 years ago
  LecygneNoir e52e7f354d Merge branch 'release/v0.9.1' 3 years ago
  LecygneNoir 701e61413c Update prismedia, documentation and dependencies to v0.9.1 3 years ago
  LecygneNoir 4e20d9efc4 Merge branch 'bypass-mime-verification' of Zykino/prismedia into develop 3 years ago
  Zykino 802d70b8d5 Empower user to force using the file they selected 3 years ago
  LecygneNoir a1c472a5fa Merge tag 'v0.9.0' into develop 4 years ago
  LecygneNoir 57a4f3dfd0 Merge branch 'release/v0.9.0' 4 years ago
  LecygneNoir 4e5c2e1245 Update Changelog for the v0.9.0 4 years ago
  LecygneNoir 1b55340b34 Update poetry.lock to match new python-magic requirements 4 years ago
  LecygneNoir 64c5378e18 Merge branch 'feature/nfo-enhanced' into develop 4 years ago
  LecygneNoir 1169301f14 After test python-magic is required on Windows in addition to python-magic-bin, so remove the 'Linux only' marker 4 years ago
  LecygneNoir 03ae92d1af Load nfo.txt or NFO.txt regardless the letter case 4 years ago
  LecygneNoir 11a91af534 Add better description for Enhanced NFO based on @Zykino review 4 years ago
  LecygneNoir 881a01f862 Write documentation about the new NFO usage and possibilities 4 years ago
  LecygneNoir ef5d6b843a Add a full set of samples to understand better who NFO works now 4 years ago
  LecygneNoir 17017ae90c Modify the NFO functions to allow an enhanced use with priorities in options 4 years ago
  LecygneNoir e91ada951f gitignore .mp4 to avoid commiting test files 4 years ago
  LecygneNoir af65627fcf Fix some typo regarding poetry 4 years ago
  LecygneNoir cb39eef8e0 Merge branch 'feature/poetry' into develop 4 years ago
  LecygneNoir 8faae852ea Add documentation for poetry and adjust Changelog 4 years ago
  LecygneNoir 72b47b95ec Modify files and add poetry configuration files to use poetry for distributing and publishing 4 years ago
  LecygneNoir b1a5d244d4 Prepare poetry with more standard nomination and configuration and change gitignore to avoid committing file by error 4 years ago
  LecygneNoir 429ea2333e Add new feature to schedule different publication date depending of the platform. Target v0.9.0, Fix #43 4 years ago
  LecygneNoir 159ab00cc9 Merge tag '0.8.0' into develop 4 years ago
  LecygneNoir 1dd41f0c46 Merge branch 'release/0.8.0' 4 years ago
  LecygneNoir 04e5c326ee Update version to 0.8.0 4 years ago
  LecygneNoir 99eee2363b Test if using python3 at the beginning of the script, and change shebang to do not force python3, as it enforce default python3 value and thus the check failed even if python2 is used 4 years ago
  LecygneNoir 76e379ab97 Merge branch 'feature/python3' into develop 4 years ago
  LecygneNoir 1e72033846 Update changelog, help and readme to finish the python3 feature 4 years ago
  LecygneNoir 2a624e1d9b Uncomment upload lines commented by error 4 years ago
  LecygneNoir 2b00d65546 Merge branch 'feature/python3-windows' of Zykino/prismedia into feature/python3 4 years ago
  Zykino 77bcda7a79 Add a debug option. Upload to Peertube before Youtube. 4 years ago
  Zykino 591ed0ab80 Fix some the README: sync the dependency list with requirements.txt 4 years ago
  Zykino e94b48278a Force the NFO parsing to read as UTF-8 4 years ago
  Zykino 8c99747898 Remove the CLI input decoding 4 years ago
  Zykino aa7aeed688 Force the use of at least Python 3 4 years ago
  Zykino 4f2a69e025 Add the binaries for the lib needed on Windows 4 years ago
  LecygneNoir aafa71ce6d Update Changelog about python3 4 years ago
  LecygneNoir ee92ff3a6f Add a requirements.txt for an easier installation of dependencies 4 years ago
  LecygneNoir fa633ee5bb Update files, functions and code to work with python3 4 years ago
  LecygneNoir 8b26f0ee53 Merge tag 'v0.7.1' into develop 4 years ago
  LecygneNoir 7b0c543865 Merge branch 'hotfix/v0.7.1' 4 years ago
  LecygneNoir e5c8c4c9b9 Release hotfix v0.7.1 4 years ago
  LecygneNoir 5dc6c78211 Fix bug #42 , crash on Peertube when there is only one tag in video 4 years ago
  LecygneNoir 83a1d30c1c Merge tag 'v0.7.0' into develop 4 years ago
  LecygneNoir 1fc0577ce7 Merge branch 'release/v0.7.0' 4 years ago
  LecygneNoir ee2e11b788 Release prismedia v0.7.0, close #40 4 years ago
  LecygneNoir 9c72a563bd Merge branch 'feature/pt-channel' into develop 4 years ago
  LecygneNoir aa81f13973 Update nfo and readme for the new channel feature for peertube 4 years ago
  LecygneNoir 44875b3567 Add new feature to combine channel and playlist on peertube 🎉 4 years ago
  LecygneNoir 322774a214 Rework README's feature list for more lisibility 4 years ago
  LecygneNoir b21317ec7e Merge branch 'hotfix/v0.6.4' into develop 4 years ago
  LecygneNoir 4ea1daf966 Merge branch 'hotfix/v0.6.4' 4 years ago
  LecygneNoir ef30541688 Update version to v0.6.4 4 years ago
  LecygneNoir 42a20308f0 Exit the upload when failing to add video to a playlist on Youtube, to be more consistent with Peertube, fix #33 4 years ago
  LecygneNoir 070e05de0b Correctly check if a playlist has been created on Youtube before trying to add an uploaded video to it, cf #33 4 years ago
  LecygneNoir b29b9cedef Patch the check for playlist exist on Youtube, fix #39 4 years ago
  LecygneNoir 6f67de0a3d Merge branch 'hotfix/0.6.3' into develop 4 years ago
  LecygneNoir 6b1260c7bc Merge branch 'hotfix/0.6.3' 4 years ago
  LecygneNoir 384a50ca63 Changelog and bump for hotfix 0.6.3 4 years ago
  LecygneNoir fff3b50074 Add the default channel_id when creating a public Peertube playlist, cf #38 4 years ago
  LecygneNoir 91bdfafb45 Merge branch 'release/v0.6.2' 4 years ago
  LecygneNoir 2a8449afec Merge branch 'feature/pt-playlist-1.3' into develop 4 years ago
  LecygneNoir 4707632f13 Add CHANGELOG for version v0.6.2 4 years ago
  LecygneNoir 82fd09c0e7 Peertube: Add the function to set playlist for peertube video, and not use channel anymore 4 years ago
  LecygneNoir 9b3d793975 Peertube: modify upload to ever use default channel and create true playlist instead. Playlist created as private for the moment 4 years ago
  LecygneNoir 3b38290040 Update CHANGELOG according to the text used in gitea for v0.6.1-1 5 years ago
  LecygneNoir 8f0fc4cfb5 Merge branch 'hotfix/v0.6.1-1' into develop 5 years ago
  LecygneNoir 7db1ad2836 Release hotfix v0.6.1-1: prepare python3 compatibility 5 years ago
  LecygneNoir 2f40ef1826 Simplify cleanString function to prepare python3 compatibility 5 years ago
  LecygneNoir 3797c9a9f0 Remove mt option as it's not useful anymore 5 years ago
  LecygneNoir 5907859066 Merge branch 'feature/encoding' of Zykino/prismedia into develop 5 years ago
  Zykino bddf2ee414 fix typo 5 years ago
  Zykino f66ba6cc21 string from URF-8 encoded files have not the "unicode" type 5 years ago
  Zykino 8d8898aa55 The strings arguments should be in unicode 5 years ago
  Zykino 6c68c3363b prevent decoding unicode strings since python prefer to crash than doing nothing 5 years ago
  Zykino 617e989154 fix error message 5 years ago
  Zykino dbcd2ff010 update the README 5 years ago
  Zykino dffd3ffa84 decode stdin strins arguments 5 years ago
  LecygneNoir 7e4f9d995c correct some typo in output, fix #23 5 years ago
  LecygneNoir 8c0f1fd038 Merge branch 'hotfix/v0.6.1' into develop 5 years ago
  LecygneNoir 08416d2796 Merge branch 'hotfix/v0.6.1' 5 years ago
  LecygneNoir 2338188325 Add changelog and version for hotfix v0.6.1 5 years ago
  LecygneNoir 9426ca465c Fix 409 conflict in Peertube when a playlist with same name already exists (#20) 5 years ago
  LecygneNoir 70a933f48a Merge branch 'hotfix/issue19' into develop 5 years ago
  LecygneNoir 097ff965bb Merge branch 'hotfix/issue19' 5 years ago
  LecygneNoir 90d998a64a Patch error on Peertube when playlist name contains non letter characters surrounded by space. fix #19 5 years ago
  LecygneNoir f8d1fb8e33 fix unicode comparison in pt_upload 5 years ago
  LecygneNoir ce671dab8e Merge branch 'release/v0.6' into develop 5 years ago
  LecygneNoir 9aa84aa8b7 Merge branch 'release/v0.6' 5 years ago
  LecygneNoir bb61725e62 Prepare documentation and changelog for release v0.6 5 years ago
  LecygneNoir 041a8fd722 Prepare documentation and changelog for release v0.6 5 years ago
  LecygneNoir 81a183dd72 fix max lenght for playlist name in Peertube 5 years ago
  LecygneNoir 745548abba Add -f as an alias for --file in command line, fix #16 5 years ago
  LecygneNoir 04514c86e6 Merge branch 'feature/playlist' into develop 5 years ago
  LecygneNoir 20d1ab6a11 Use console instead of local_server for authentication in Youtube to enable scripting 5 years ago
  LecygneNoir 75aa36e1aa Check if scopes are corrects in youtube credentials 5 years ago
  LecygneNoir 070408bb67 Add playlist feature to Youtube 5 years ago
  LecygneNoir d3a42c4be1 Patch playlist to be comaptible with beta16 (using name AND display name) 5 years ago
  LecygneNoir 2565071e40 Add examples for using playlist feature 5 years ago
  LecygneNoir 64908a75ca Merge branch 'develop' into feature/playlist 5 years ago
  LecygneNoir d744ccb45e Update changelog for new publishAt feature 5 years ago
  LecygneNoir db94d96035 Merge branch 'publishAt' of Zykino/prismedia into develop 5 years ago
  LecygneNoir b4691bba1e Correct typo in readme for playlist feature 5 years ago
  LecygneNoir 8d24bfa6a1 Merge remote-tracking branch 'zykino/feature/playlist' into feature/playlist 5 years ago
  Zykino 5d59889f82 Remove external utilities in README not used anymore for to publishAt 5 years ago
  Zykino 8d02d5a3a1 Update peertube "publish at" functionnality to use their API 5 years ago
  Zykino 2d7b8e0b09 Update README to show the current state of the playlist integration 5 years ago
  Zykino 95f6bc930f fix merge: function name changed 5 years ago
  LecygneNoir 6c13370e80 Merge branch 'feature/playlist' of ssh://git.lecygnenoir.info:2223/LecygneNoir/prismedia into feature/playlist 5 years ago
  LecygneNoir 9efb18eb55 Add option to manage playlist for videos 5 years ago
  LecygneNoir cb8ae77a10 Add support for playlist on Peertube 5 years ago
  LecygneNoir 972cfd73d3 Update peeror repo with new url 5 years ago
  LecygneNoir 56873c0063 Merge branch 'fixFirstSetup' of Zykino/prismedia into develop 5 years ago
  Zykino 461beaa5cb peertube: fix default playlist 5 years ago
  LecygneNoir 9118f7b082 Add option to manage playlist for videos 5 years ago
  LecygneNoir bd8aa9c485 Add support for playlist on Peertube 5 years ago
  LecygneNoir c5aeacb936 Update peeror repo with new url 5 years ago
  Zykino 3b76221f29 update shebang to use python2 instead of the distrb prefered 5 years ago
  Zykino 34103c49f2 print the peertube response.json when in error 5 years ago
  Zykino 844173f326 fix OAuth URL for peertube 5 years ago
  Zykino f365eb1089 fix README with the version of python used and adding a missing dependency 5 years ago
  LecygneNoir bd2631699a Add option to manage playlist for videos 5 years ago
  LecygneNoir 68959cc1a8 Add support for playlist on Peertube 5 years ago
  LecygneNoir 6a07bbbf4b Update peeror repo with new url 5 years ago
  LecygneNoir e4e7abb39d Merge branch 'release/v0.5' 5 years ago
37 changed files with 3331 additions and 1005 deletions
Split View
  1. +5
    -1
      .gitignore
  2. +144
    -2
      CHANGELOG.md
  3. +13
    -0
      PLUGINS.md
  4. +181
    -119
      README.md
  5. +0
    -161
      lib/pt_upload.py
  6. +0
    -263
      lib/utils.py
  7. +0
    -202
      lib/yt_upload.py
  8. +0
    -24
      nfo_example.txt
  9. +577
    -0
      poetry.lock
  10. +12
    -0
      prismedia/__init__.py
  11. +2
    -0
      prismedia/__main__.py
  12. +58
    -0
      prismedia/configuration.py
  13. +254
    -0
      prismedia/core.py
  14. +24
    -0
      prismedia/genconfig.py
  15. +72
    -0
      prismedia/pluginInterfaces.py
  16. +9
    -0
      prismedia/plugins/consumers/debug.prismedia-plugin
  17. +23
    -0
      prismedia/plugins/consumers/debug.py
  18. +10
    -0
      prismedia/plugins/interfaces/cli.prismedia-plugin
  19. +68
    -0
      prismedia/plugins/interfaces/cli.py
  20. +9
    -0
      prismedia/plugins/interfaces/help.prismedia-plugin
  21. +51
    -0
      prismedia/plugins/interfaces/help.py
  22. +11
    -0
      prismedia/plugins/platforms/peertube.prismedia-plugin
  23. +408
    -0
      prismedia/plugins/platforms/peertube.py
  24. +397
    -0
      prismedia/plugins/platforms/youtube.py
  25. +12
    -0
      prismedia/samples/cli_nfo.txt
  26. +26
    -0
      prismedia/samples/full_nfo_examples.txt
  27. +10
    -0
      prismedia/samples/nfo.txt
  28. +2
    -2
      prismedia/samples/peertube_secret.sample
  29. +14
    -0
      prismedia/samples/samples.txt
  30. +14
    -0
      prismedia/samples/yourvideo.txt
  31. +0
    -0
      prismedia/samples/youtube_secret.json.sample
  32. +350
    -0
      prismedia/upload.py
  33. +260
    -0
      prismedia/utils.py
  34. +123
    -0
      prismedia/video.py
  35. +0
    -231
      prismedia_upload.py
  36. +51
    -0
      pyproject.toml
  37. +141
    -0
      requirements.txt

+ 5
- 1
.gitignore View File

@ -61,4 +61,8 @@ target/
# Project
youtube_secret.json
peertube_secret
.youtube_credentials.json
.youtube_credentials.json
nfo_example.txt
peertube_secret.sample
youtube_secret.json.sample
*.mp4

+ 144
- 2
CHANGELOG.md View File

@ -1,9 +1,151 @@
# Changelog
## v 0.?
## v0.11.0
## Features
- Add the configuration of Original date of Record for Youtube and Peertube (see #50)
- Add a progress bar when uploading on Peertube (Thanks @Zykino, see #52)
## v0.10.3
### Fix
- Fix the pagination for Peertube playlist, as index begins at 0, not 1
## v0.10.2
### Fixes
- Fix a typo in log (missing space when displaying thumbnail) (see #49)
- Add pagination when searching playlist in Peertube as default pagination show only 14 playlists (see #41)
- Add a check to avoid uploading video on Peertube with more than 5 tags (see #48)
- Revert the workaround for Youtube playlist bug now the bug is fixed by Youtube (see #47)
## v0.10.1
### Fix
- Fix a bug introduced with v0.10.0 that broke thumbnail on youtube upload.
## v0.10.0
### Features
- Add the possibility to specify strict checks option to never forgot parameters when uploading (see #36)
- Improve logging system, add options for batch upload and print url-only in the stdout (see #29)
- --debug option is now deprecated in favor of --log=debug
### Fixes
- Workaround against the Youtube API breakdown while adding video in playlist. See #47 for details. Should be removed once Google fix their bugs.
## v0.9.1
### Features
- Possibility to bypass the MIME check for .mp4 when the user is sure of its video (#46 , thanks to @zykino)
- Now **available with pip** for installation! (See the README for doc)
## v0.9.0
### Upgrade from v0.8.0
Now using [poetry](https://python-poetry.org/) for packaging and installing! It's easier to maintain and publish package, but means changes when using prismedia from command line.
**Using poetry** (recommanded)
- [install poetry](https://python-poetry.org/docs/#installation)
- git pull the repo
- install prismedia:
```bash
poetry install
```
- use prismedia from the command line directly from your path:
```bash
prismedia -h
```
**From source**
Prismedia is now seen as a python module, so you need to use `python -m prismedia` instead of `./prismedia_upload.py`.
Once you have pulled the new v0.9.0, you may update by using:
```
pip install -r requirements.txt
# Then use prismedia through python command line:
python -m prismedia -h
```
### Features
- Prismedia now uses [poetry](https://python-poetry.org) to allow easier installation usage and build, see the README (fix #34)
- Add two new options to schedule video by platform. You may now use youtubeAt and peertubeAt to prepare previews (fix #43)
- Enhance the NFO system to allow a hierarchical loading of multiple NFO, with priorities. See README and [prismedia/samples](prismedia/samples) for details (fix #11)
## v0.8.0
### Breaking changes
Now work with python 3! Support of python 2 is no longer available.
You should now use python 3 in order to use prismedia
### Features
- Add a requirements.txt file to make installing requirement easier.
- Add a debug option to show some infos before uploading (thanks to @zykino)
- Now uploading to Peertube before Youtube (thanks to @zykino)
## v0.7.1
### Fixes
Fix bug #42 , crash on Peertube when video has only one tag
## v0.7.0
### Features
Support Peertube channel additionally with playlist for Peertube!
Peertube only as channel are Peertube's feature. See #40 for details.
### Fixes
- Best uses of special chars in videoname, channel name and playlist name
- Some fixes in logging message for better lisibility
- Readme features list improved for better lisibility
## v0.6.4
### Fixes
- Fix #33, no more trying to add a video into a playlist when the playlist does not exist on Youtube
- fix #39, patch the playlist name check when playlist contains special chars
## v0.6.3
### Fixes
Fix Critical bug #38 that prevent upload when creating playlists on Peertube, as public playlist need a non-null channel_id.
## v0.6.2
**Warning**: your Peertube instance should be at least in v1.3.0 to use this new functionality.
### Features
New feature, the Peertube playlists are now supported!
We do not use channel in place of playlist anymore.
## v0.6.1-1 Hotfix
This fix prepares the python3 compatibility.
**Warning** you need a new prerequisites: python-unidecode
- Remove mastodon tags (mt) options as it's deprecated. Compatibility between Peertube and Mastodon is complete.
- Simplify python2 specific functions
## v0.6.1
### Fixes
- fix an error when playlists on Peertube have same names but not same display names (issue #20)
- fix an error where videos does not upload on Peertube when some characters are used in playlist(issue #19)
## v0.6
### Compatibility ###
**Beware**, the first launch of prismedia for youtube will reask for credentials, this is needed for playlists.
This release is fully compatible with Peertube v1.0.0!
### Features
- Add the possibility to upload thumbnail
- Add the possibility to upload thumbnail.
- Add the possibility to configure playlist. (thanks @zykino for Peertube part)
- Use the API instead of external binaries for publishAt for both Peertube and Youtube. (thanks @zykino)
- Use the console option to authenticate against youtube for easier use with ssh'ed servers
- Add -f as an alias for --file for easier upload.
## v0.5

+ 13
- 0
PLUGINS.md View File

@ -0,0 +1,13 @@
For our plugins we are using [yaspy](http://yapsy.sourceforge.net).
# Types
For an example of the exact methods required to be recognized as a particular type of plugin, see the concerned interface definition.
## Interface
Plugins that present an interface (cli, gui, configuration folders, …) for the user to tell Prismedia wich video needs to be uploaded, the infos of the videos, …
## Platform
Also called uploaders, they are the one doing the actual work of uploading video to a particular platform.
## Consumer
Thoses do actions once the upload is finished (successful or failed).

+ 181
- 119
README.md View File

@ -1,172 +1,234 @@
# Prismedia
A scripting way to upload videos to peertube and youtube
## Dependencies
Search in your package manager, otherwise use ``pip install --upgrade``
- google-auth
- google-auth-oauthlib
- google-auth-httplib2
- google-api-python-client
- docopt
- schema
- python-magic
- requests-toolbelt
- tzlocal
For Peertube and if you want to use the publishAt option, you also need some utilities on you local system
- [atd](https://linux.die.net/man/8/atd) daemon
- [curl](https://linux.die.net/man/1/curl)
- [jq](https://stedolan.github.io/jq/)
Scripting your way to upload videos to peertube and youtube. Works with Python 3.5+.
[TOC]: #
## Table of Contents
- [Installation](#installation-and-upgrade)
- [From pip](#from-pip)
- [From source](#from-source)
- [Configuration](#configuration)
- [Peertube](#peertube)
- [Youtube](#youtube)
- [Usage](#usage)
- [Enhanced use of NFO](#enhanced-use-of-nfo)
- [Strict check options](#strict-check-options)
- [Features](#features)
- [Compatibility](#compatibility)
- [Inspirations](#inspirations)
- [Contributors](#contributors)
## Installation and upgrade
### From pip
Simply install with
```sh
pip install prismedia
```
Upgrade with
```sh
pip install --upgrade prismedia
```
### From source
Get the source:
```sh
git clone https://git.lecygnenoir.info/LecygneNoir/prismedia.git prismedia
```
You may use pip to install requirements: `pip install -r requirements.txt` if you want to use the script directly.
(**note:** requirements are generated via `poetry export -f requirements.txt --output requirements.txt`)
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
(Or use the existing virtualenv if one is activated)
```sh
poetry install
```
## Configuration
Edit peertube_secret and youtube_secret.json with your credentials.
Generate configuration files by running `prismedia-init`.
Then, edit them to fill your credential as explained below.
### Peertube
Set your credentials, peertube server URL.
You can get client_id and client_secret by logging in your peertube website and reaching the URL: https://domain.example/api/v1/oauth-clients/local
You can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)
Configuration is in **peertube_secret** file.
You need your usual credentials and Peertube instance URL, in addition with API client_id and client_secret.
You can get client_id and client_secret by logging in your peertube instance and reaching the URL:
https://domain.example/api/v1/oauth-clients/local
### Youtube
Configuration is in **youtube_secret.json** file.
Youtube uses combination of oauth and API access to identify.
**Credentials**
The first time you connect, prismedia will open your browser to as you to authenticate to
The first time you connect, prismedia will open your browser to ask you to authenticate to
Youtube and allow the app to use your Youtube channel.
**It is here you choose which channel you will upload to**.
Once authenticated, the token is stored inside the file ``.youtube_credentials.json``.
Once authenticated, the token is stored inside the file `.youtube_credentials.json`.
Prismedia will try to use this file at each launch, and re-ask for authentication if it does not exist.
**Oauth**:
The default youtube_secret.json should allow you to upload some videos.
If you plan an larger usage, please consider creating your own youtube_secret file:
- Go to the [Google console](https://console.developers.google.com/).
- Create project.
- Side menu: APIs & auth -> APIs
- Top menu: Enabled API(s): Enable all Youtube APIs.
- Side menu: APIs & auth -> Credentials.
- Create a Client ID: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: prismedia1 -> Create -> OK
- Download JSON: Under the section "OAuth 2.0 client IDs". Save the file to your local system.
- Save this JSON as your youtube_secret.json file.
## How To
Currently in heavy development
Support only mp4 for cross compatibility between Youtube and Peertube
Simply upload a video:
If you plan a larger usage, please consider creating your own youtube_secret file:
- Go to the [Google console](https://console.developers.google.com/).
- Create project.
- Side menu: APIs & auth -> APIs
- Top menu: Enabled API(s): Enable all Youtube APIs.
- Side menu: APIs & auth -> Credentials.
- Create a Client ID: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: prismedia1 -> Create -> OK
- Download JSON: Under the section "OAuth 2.0 client IDs". Save the file to your local system.
- Save this JSON as your youtube_secret.json file.
## Usage
Support only mp4 for cross compatibility between Youtube and Peertube.
**Note that all options may be specified in a NFO file!** (see [Enhanced NFO](#enhanced-use-of-nfo))
Here are some demonstration of main usage:
Upload a video:
```sh
prismedia --file="yourvideo.mp4"
```
./prismedia_upload.py --file="yourvideo.mp4"
```
Specify description and tags:
```
./prismedia_upload.py --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
```sh
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
```
Provide a thumbnail:
```sh
prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
```
./prismedia_upload.py --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
Publish on Peertube only, while using a channel and a playlist, creating them if they do not exist:
```sh
prismedia --file="yourvideo.mp4" --platform=peertube --channel="Cooking recipes" --playlist="Cake recipes" --channelCreate --playlistCreate
```
Use a NFO file to specify your video options:
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
```sh
prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
```
Use a NFO file to specify your video options:
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).
```
./prismedia_upload.py --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
To prevent Youtube from inactivating your apikey after 90days of inactivity it is recommended to launch this command automatically from a script around once a month. It will mwke a call to use a few credits from your daily quota.
On Linux and MacOS, you can use cron, on Windows the "Task Scheduler".
```sh
prismedia --hearthbeat
```
Take a look at all available options with `--help`!
```sh
prismedia --help
```
Use --help to get all available options:
## Enhanced use of NFO
Since Prismedia v0.9.0, the NFO system has been improved to allow hierarchical loading.
First, **if you already used nfo**, either with `--nfo` or by using `videoname.txt`, nothing changes :-)
But you are now able to use a more flexible NFO system, by using priorities. This allows you to set some defaults to avoid recreating a full nfo for each video
Basically, Prismedia will now load options in this order, using the last value found in case of conflict:
`nfo.txt < directory_name.txt < video_name.txt < command line NFO < command line argument`
You'll find a complete set of samples in the [prismedia/samples](prismedia/samples) directory so let's take it as an example:
```sh
$ tree Recipes/
Recipes/
├── cli_nfo.txt
├── nfo.txt
├── samples.txt
├── yourvideo1.mp4
├── yourvideo1.txt
├── yourvideo1.jpg
├── yourvideo2.mp4
└── yourvideo2.txt
```
prismedia_upload - tool to upload videos to Peertube and Youtube
Usage:
prismedia_upload.py --file=<FILE> [options]
prismedia_upload.py --file=<FILE> --tags=STRING [--mt options]
prismedia_upload.py -h | --help
prismedia_upload.py --version
Options:
--name=NAME Name of the video to upload. (default to video filename)
-d, --description=STRING Description of the video. (default: default description)
-t, --tags=STRING Tags for the video. comma separated.
WARN: tags with space and special characters (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
use mastodon compatibility below
--mt Force Mastodon compatibility for tags (drop every incompatible characters inside tags)
This option requires --tags
-c, --category=STRING Category for the videos, see below. (default: Films)
--cca License should be CreativeCommon Attribution (affects Youtube upload only)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
--nfo=STRING Configure a specific nfo file to set options for the video.
By default Prismedia search a .txt based on video name
See nfo_example.txt for more details
--platform=STRING List of platform(s) to upload to, comma separated.
Supported platforms are youtube and peertube (default is both)
--language=STRING Specify the default language for video. See below for supported language. (default is English)
--publishAt=DATE Publish the video at the given DATE using local server timezone.
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future
For Peertube, requires the "atd" and "curl utilities installed on the system
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
-h --help Show this help.
--version Show version.
Categories:
Category is the type of video you upload. Default is films.
Here are available categories from Peertube and Youtube:
music, films, vehicles,
sports, travels, gaming, people,
comedy, entertainment, news,
how to, education, activism, science & technology,
science, technology, animals
Languages:
Language of the video (audio track), choose one. Default is English
Here are available languages from Peertube and Youtube:
Arabic, English, French, German, Hindi, Italian,
Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
By using
```sh
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
```
Prismedia will:
- look for options in `nfo.txt`
- look for options in `samples.txt` (from directory name) and erase any previous conflicting options
- look for options in `yourvideo1.txt` (from video name) and erase any previous conflicting options
- look for options in `cli_nfo.txt` (from the `--nfo` in command line) and erase any previous conflicting options
- erase any previous option regarding CCA as it's specified in cli with `--cca`
- take `yourvideo1.jpg` as thumbnail if no other files has been specified in previous NFO
In other word, Prismedia will use option given in cli, then look for option in cli_nfo.txt, then complete with video_name.txt, then directory_name.txt, and finally complete with nfo.txt
It allows to specify more easily default options for an entire set of video, directory, playlist and so on.
## Strict check options
Since prismedia v0.10.0, a bunch of special options have been added to force the presence of parameters before uploading.
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
tags, thumbnail, ...
All strict option are optionals and are provided only to avoid errors when uploading :-)
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
All strict options are off by default.
Available strict options:
- --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory
- --withThumbnail Prevent the upload without a thumbnail
- --withName Prevent the upload if no name are found
- --withDescription Prevent the upload without description
- --withTags Prevent the upload without tags
- --withPlaylist Prevent the upload if no playlist
- --withPublishAt Prevent the upload if no schedule
- --withPlatform Prevent the upload if at least one platform is not specified
- --withCategory Prevent the upload if no category
- --withLanguage Prevent upload if no language
- --withChannel Prevent upload if no channel
## Features
- [x] Youtube upload
- [x] Peertube upload
- Support of all videos arguments (description, tags, category, licence, ...)
- Support of videos parameters (description, tags, category, licence, ...)
- [x] description
- [x] tags (no more than 30 characters per tag as Peertube does not support it)
- [x] Option to force tags to be compatible with Mastodon publication
- [x] categories
- [x] license: cca or not (Youtube only as Peertube uses Attribution by design)
- [x] privacy (between public, unlisted or private)
- [x] enabling/disabling comment (Peertube only as Youtube API does not support it)
- [x] nsfw (Peertube only as Youtube API does not support it)
- [x] set default language
- [x] thumbnail/preview
- [x] thumbnail
- [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
- [ ] add videos to playlist (YT & PT workflow: upload video, find playlist id, add video to playlist)
- [x] add videos to playlist
- [x] create playlist
- [x] schedule your video with publishAt
- [x] combine channel and playlist (Peertube only as channel is Peertube feature). See [issue 40](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/40) for detailed usage.
- [x] Use a config file (NFO) file to retrieve videos arguments
- [x] Allow to choose peertube or youtube upload (to resume failed upload for example)
- [x] Add publishAt option to plan your videos (need the [atd](https://linux.die.net/man/8/atd) daemon, [curl](https://linux.die.net/man/1/curl) and [jq](https://stedolan.github.io/jq/))
- [ ] Record and forget: put the video in a directory, and the script uploads it for you
- [ ] Usable on Desktop (Linux and/or Windows and/or MacOS)
- [ ] Graphical User Interface
- [x] Allow choosing peertube or youtube upload (to retry a failed upload for example)
- [x] Usable on Desktop (Linux and/or Windows and/or MacOS)
- [x] Different schedules on platforms to prepare preview
- [x] Possibility to force the presence of upload options
- [ ] Copy and forget, eg possibility to copy video in a directory, and prismedia uploads itself: [Work in progress](https://git.lecygnenoir.info/Zykino/prismedia-autoupload) thanks to @Zykino 🎉 (Discussions in [issue 27](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/27))
- [ ] A usable graphical interface
## Compatibility
If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
- If you still use python2, use the version 0.7.1 (no more updated)
- If you use peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3
## Inspirations
Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Sources
inspired by [peeror](https://git.drycat.fr/rigelk/Peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Contributors
Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘

+ 0
- 161
lib/pt_upload.py View File

@ -1,161 +0,0 @@
#!/usr/bin/python
# coding: utf-8
import os
import mimetypes
import json
import logging
from os.path import splitext, basename, abspath
from ConfigParser import RawConfigParser
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import LegacyApplicationClient
from requests_toolbelt.multipart.encoder import MultipartEncoder
import utils
PEERTUBE_SECRETS_FILE = 'peertube_secret'
PEERTUBE_PRIVACY = {
"public": 1,
"unlisted": 2,
"private": 3
}
def get_authenticated_service(secret):
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
oauth_client = LegacyApplicationClient(
client_id=str(secret.get('peertube', 'client_id'))
)
try:
oauth = OAuth2Session(client=oauth_client)
oauth.fetch_token(
token_url=str(peertube_url + '/api/v1/users/token'),
# lower as peertube does not store uppercase for pseudo
username=str(secret.get('peertube', 'username').lower()),
password=str(secret.get('peertube', 'password')),
client_id=str(secret.get('peertube', 'client_id')),
client_secret=str(secret.get('peertube', 'client_secret'))
)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Peertube: Error: " + str(e.message))
exit(1)
else:
logging.error("Peertube: Error: " + str(e))
exit(1)
return oauth
def upload_video(oauth, secret, options):
def get_userinfo():
user_info = json.loads(oauth.get(url + "/api/v1/users/me").content)
return str(user_info["id"])
def get_file(path):
mimetypes.init()
return (basename(path), open(abspath(path), 'rb'),
mimetypes.types_map[splitext(path)[1]])
url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
# We need to transform fields into tuple to deal with tags as
# MultipartEncoder does not support list refer
# https://github.com/requests/toolbelt/issues/190 and
# https://github.com/requests/toolbelt/issues/205
fields = [
("name", options.get('--name') or splitext(basename(options.get('--file')))[0]),
("licence", "1"),
("description", options.get('--description') or "default description"),
("nsfw", str(int(options.get('--nsfw')) or "0")),
("channelId", get_userinfo()),
("videofile", get_file(options.get('--file')))
]
if options.get('--tags'):
tags = options.get('--tags').split(',')
for strtag in tags:
# Empty tag crashes Peertube, so skip them
if strtag == "":
continue
# Tag more than 30 chars crashes Peertube, so exit and check tags
if len(strtag) >= 30:
logging.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce your tag size")
exit(1)
# If Mastodon compatibility is enabled, clean tags from special characters
if options.get('--mt'):
strtag = utils.mastodonTag(strtag)
fields.append(("tags", strtag))
if options.get('--category'):
fields.append(("category", str(utils.getCategory(options.get('--category'), 'peertube'))))
else:
# if no category, set default to 2 (Films)
fields.append(("category", "2"))
if options.get('--language'):
fields.append(("language", str(utils.getLanguage(options.get('--language'), "peertube"))))
else:
# if no language, set default to 1 (English)
fields.append(("language", "en"))
if options.get('--privacy'):
fields.append(("privacy", str(PEERTUBE_PRIVACY[options.get('--privacy').lower()])))
else:
fields.append(("privacy", "3"))
if options.get('--disable-comments'):
fields.append(("commentsEnabled", "0"))
else:
fields.append(("commentsEnabled", "1"))
if options.get('--thumbnail'):
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
fields.append(("previewfile", get_file(options.get('--thumbnail'))))
multipart_data = MultipartEncoder(fields)
headers = {
'Content-Type': multipart_data.content_type
}
response = oauth.post(url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['video']
uuid = jresponse['uuid']
idvideo = str(jresponse['id'])
logging.info('Peertube : Video was successfully uploaded.')
template = 'Peertube: Watch it at %s/videos/watch/%s.'
logging.info(template % (url, uuid))
if options.get('--publishAt'):
utils.publishAt(str(options.get('--publishAt')), oauth, url, idvideo, secret)
else:
logging.error(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
def run(options):
secret = RawConfigParser()
try:
secret.read(PEERTUBE_SECRETS_FILE)
except Exception as e:
logging.error("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
exit(1)
insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT')
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport
oauth = get_authenticated_service(secret)
try:
logging.info('Peertube: Uploading video...')
upload_video(oauth, secret, options)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Peertube: Error: " + str(e.message))
else:
logging.error("Peertube: Error: " + str(e))

+ 0
- 263
lib/utils.py View File

@ -1,263 +0,0 @@
#!/usr/bin/python
# coding: utf-8
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
from os.path import dirname, splitext, basename, isfile
from os import devnull
from subprocess import check_call, CalledProcessError, STDOUT
import unicodedata
import logging
### CATEGORIES ###
YOUTUBE_CATEGORY = {
"music": 10,
"films": 1,
"vehicles": 2,
"sport": 17,
"travels": 19,
"gaming": 20,
"people": 22,
"comedy": 23,
"entertainment": 24,
"news": 25,
"how to": 26,
"education": 27,
"activism": 29,
"science & technology": 28,
"science": 28,
"technology": 28,
"animals": 15
}
PEERTUBE_CATEGORY = {
"music": 1,
"films": 2,
"vehicles": 3,
"sport": 5,
"travels": 6,
"gaming": 7,
"people": 8,
"comedy": 9,
"entertainment": 10,
"news": 11,
"how to": 12,
"education": 13,
"activism": 14,
"science & technology": 15,
"science": 15,
"technology": 15,
"animals": 16
}
### LANGUAGES ###
YOUTUBE_LANGUAGE = {
"arabic": 'ar',
"english": 'en',
"french": 'fr',
"german": 'de',
"hindi": 'hi',
"italian": 'it',
"japanese": 'ja',
"korean": 'ko',
"mandarin": 'zh-CN',
"portuguese": 'pt-PT',
"punjabi": 'pa',
"russian": 'ru',
"spanish": 'es'
}
PEERTUBE_LANGUAGE = {
"arabic": "ar",
"english": "en",
"french": "fr",
"german": "de",
"hindi": "hi",
"italian": "it",
"japanese": "ja",
"korean": "ko",
"mandarin": "zh",
"portuguese": "pt",
"punjabi": "pa",
"russian": "ru",
"spanish": "es"
}
######################
def getCategory(category, platform):
if platform == "youtube":
return YOUTUBE_CATEGORY[category.lower()]
else:
return PEERTUBE_CATEGORY[category.lower()]
def getLanguage(language, platform):
if platform == "youtube":
return YOUTUBE_LANGUAGE[language.lower()]
else:
return PEERTUBE_LANGUAGE[language.lower()]
def remove_empty_kwargs(**kwargs):
good_kwargs = {}
if kwargs is not None:
for key, value in kwargs.iteritems():
if value:
good_kwargs[key] = value
return good_kwargs
def searchThumbnail(options):
video_directory = dirname(options.get('--file')) + "/"
# First, check for thumbnail based on videoname
if options.get('--name'):
if isfile(video_directory + options.get('--name') + ".jpg"):
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
elif isfile(video_directory + options.get('--name') + ".jpeg"):
options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
if not options.get('--thumbnail'):
video_file = splitext(basename(options.get('--file')))[0]
if isfile(video_directory + video_file + ".jpg"):
options['--thumbnail'] = video_directory + video_file + ".jpg"
elif isfile(video_directory + video_file + ".jpeg"):
options['--thumbnail'] = video_directory + video_file + ".jpeg"
return options
# return the nfo as a RawConfigParser object
def loadNFO(options):
video_directory = dirname(options.get('--file')) + "/"
if options.get('--nfo'):
try:
logging.info("Using " + options.get('--nfo') + " as NFO, loading...")
if isfile(options.get('--nfo')):
nfo = RawConfigParser()
nfo.read(options.get('--nfo'))
return nfo
else:
logging.error("Given NFO file does not exist, please check your path.")
exit(1)
except Exception as e:
logging.error("Problem with NFO file: " + str(e))
exit(1)
else:
if options.get('--name'):
nfo_file = video_directory + options.get('--name') + ".txt"
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser()
nfo.read(nfo_file)
return nfo
except Exception as e:
logging.error("Problem with NFO file: " + str(e))
exit(1)
# if --nfo and --name does not exist, use --file as default
video_file = splitext(basename(options.get('--file')))[0]
nfo_file = video_directory + video_file + ".txt"
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser()
nfo.read(nfo_file)
return nfo
except Exception as e:
logging.error("Problem with nfo file: " + str(e))
exit(1)
logging.info("No suitable NFO found, skipping.")
return False
def parseNFO(options):
nfo = loadNFO(options)
if nfo:
# We need to check all options and replace it with the nfo value if not defined (None or False)
for key, value in options.iteritems():
key = key.replace("-", "")
try:
# get string options
if value is None and nfo.get('video', key):
options['--' + key] = nfo.get('video', key)
# get boolean options
elif value is False and nfo.getboolean('video', key):
options['--' + key] = nfo.getboolean('video', key)
except NoOptionError:
continue
except NoSectionError:
logging.error("Given NFO file miss section [video], please check syntax of your NFO.")
exit(1)
return options
def upcaseFirstLetter(s):
return s[0].upper() + s[1:]
def publishAt(publishAt, oauth, url, idvideo, secret):
try:
FNULL = open(devnull, 'w')
check_call(["at", "-V"], stdout=FNULL, stderr=STDOUT)
except CalledProcessError:
logging.error("You need to install the atd daemon to use the publishAt option.")
exit(1)
try:
FNULL = open(devnull, 'w')
check_call(["curl", "-V"], stdout=FNULL, stderr=STDOUT)
except CalledProcessError:
logging.error("You need to install the curl command line to use the publishAt option.")
exit(1)
try:
FNULL = open(devnull, 'w')
check_call(["jq", "-V"], stdout=FNULL, stderr=STDOUT)
except CalledProcessError:
logging.error("You need to install the jq command line to use the publishAt option.")
exit(1)
time = publishAt.split("T")
# Remove leading seconds that atd does not manage
if time[1].count(":") == 2:
time[1] = time[1][:-3]
atTime = time[1] + " " + time[0]
refresh_token=str(oauth.__dict__['_client'].__dict__['refresh_token'])
atFile = "/tmp/peertube_" + idvideo + "_" + publishAt + ".at"
try:
openfile = open(atFile,"w")
openfile.write('token=$(curl -X POST -d "client_id=' + str(secret.get('peertube', 'client_id')) +
'&client_secret=' + str(secret.get('peertube', 'client_secret')) +
'&grant_type=refresh_token&refresh_token=' + str(refresh_token) +
'" "' + url + '/api/v1/users/token" | jq -r .access_token)')
openfile.write("\n")
openfile.write('curl "' + url + '/api/v1/videos/' + idvideo +
'" -X PUT -H "Authorization: Bearer ${token}"' +
' -H "Content-Type: multipart/form-data" -F "privacy=1"')
openfile.write("\n ") # atd needs an empty line at the end of the file to load...
openfile.close()
except Exception as e:
if hasattr(e, 'message'):
logging.error("Error: " + str(e.message))
else:
logging.error("Error: " + str(e))
try:
FNULL = open(devnull, 'w')
check_call(["at", "-M", "-f", atFile, atTime], stdout=FNULL, stderr=STDOUT)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Error: " + str(e.message))
else:
logging.error("Error: " + str(e))
def mastodonTag(tag):
tags = tag.split(' ')
mtag = ''
for s in tags:
if s == '':
continue
strtag = unicodedata.normalize('NFKD', unicode (s, 'utf-8')).encode('ASCII', 'ignore')
strtag = ''.join(e for e in strtag if e.isalnum())
strtag = upcaseFirstLetter(strtag)
mtag = mtag + strtag
return mtag

+ 0
- 202
lib/yt_upload.py View File

@ -1,202 +0,0 @@
#!/usr/bin/python
# coding: utf-8
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
import httplib
import httplib2
import random
import time
import copy
import json
from os.path import splitext, basename, exists
import google.oauth2.credentials
import datetime
import pytz
import logging
from tzlocal import get_localzone
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
from google_auth_oauthlib.flow import InstalledAppFlow
import utils
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
# Explicitly tell the underlying HTTP transport library not to retry, since
# we are handling retry logic ourselves.
httplib2.RETRIES = 1
# Maximum number of times to retry before giving up.
MAX_RETRIES = 10
# Youtube retriables cases
RETRIABLE_EXCEPTIONS = (
IOError,
httplib2.HttpLib2Error,
httplib.NotConnected,
httplib.IncompleteRead,
httplib.ImproperConnectionState,
httplib.CannotSendRequest,
httplib.CannotSendHeader,
httplib.ResponseNotReady,
httplib.BadStatusLine,
)
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
CLIENT_SECRETS_FILE = 'youtube_secret.json'
CREDENTIALS_PATH = ".youtube_credentials.json"
SCOPES = ['https://www.googleapis.com/auth/youtube.upload']
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
# Authorize the request and store authorization credentials.
def get_authenticated_service():
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES)
if exists(CREDENTIALS_PATH):
with open(CREDENTIALS_PATH, 'r') as f:
credential_params = json.load(f)
credentials = google.oauth2.credentials.Credentials(
credential_params["token"],
refresh_token=credential_params["_refresh_token"],
token_uri=credential_params["_token_uri"],
client_id=credential_params["_client_id"],
client_secret=credential_params["_client_secret"]
)
else:
credentials = flow.run_local_server()
with open(CREDENTIALS_PATH, 'w') as f:
p = copy.deepcopy(vars(credentials))
del p["expiry"]
json.dump(p, f)
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False)
def initialize_upload(youtube, options):
path = options.get('--file')
tags = None
if options.get('--tags'):
tags = options.get('--tags').split(',')
category = None
if options.get('--category'):
category = utils.getCategory(options.get('--category'), 'youtube')
language = None
if options.get('--language'):
language = utils.getLanguage(options.get('--language'), "youtube")
license = None
if options.get('--cca'):
license = "creativeCommon"
body = {
"snippet": {
"title": options.get('--name') or splitext(basename(path))[0],
"description": options.get('--description') or "default description",
"tags": tags,
# if no category, set default to 1 (Films)
"categoryId": str(category or 1),
"defaultAudioLanguage": str(language or 'en')
},
"status": {
"privacyStatus": str(options.get('--privacy') or "private"),
"license": str(license or "youtube"),
}
}
if options.get('--publishAt'):
# Youtube needs microsecond and the local timezone from ISO 8601
publishAt = options.get('--publishAt') + ".000001"
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S.%f')
tz = get_localzone()
tz = pytz.timezone(str(tz))
publishAt = tz.localize(publishAt).isoformat()
body['status']['publishAt'] = str(publishAt)
# Call the API's videos.insert method to create and upload the video.
insert_request = youtube.videos().insert(
part=','.join(body.keys()),
body=body,
media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
)
video_id = resumable_upload(insert_request, 'video', 'insert')
# If we get a video_id, upload is successful and we are able to set thumbnail
if video_id and options.get('--thumbnail'):
set_thumbnail(youtube, options.get('--thumbnail'), videoId=video_id)
def set_thumbnail(youtube, media_file, **kwargs):
kwargs = utils.remove_empty_kwargs(**kwargs) # See full sample for function
request = youtube.thumbnails().set(
media_body=MediaFileUpload(media_file, chunksize=-1,
resumable=True),
**kwargs
)
# See full sample for function
return resumable_upload(request, 'thumbnail', 'set')
# This method implements an exponential backoff strategy to resume a
# failed upload.
def resumable_upload(request, resource, method):
response = None
error = None
retry = 0
while response is None:
try:
template = 'Youtube: Uploading %s...'
logging.info(template % resource)
status, response = request.next_chunk()
if response is not None:
if method == 'insert' and 'id' in response:
logging.info('Youtube : Video was successfully uploaded.')
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
logging.info(template % response['id'])
return response['id']
elif method != 'insert' or "id" not in response:
logging.info('Youtube: Thumbnail was successfully set.')
else:
template = ('Youtube : The upload failed with an '
'unexpected response: %s')
logging.error(template % response)
exit(1)
except HttpError as e:
if e.resp.status in RETRIABLE_STATUS_CODES:
template = 'Youtube : A retriable HTTP error %d occurred:\n%s'
error = template % (e.resp.status, e.content)
else:
raise
except RETRIABLE_EXCEPTIONS as e:
error = 'Youtube : A retriable error occurred: %s' % e
if error is not None:
logging.warning(error)
retry += 1
if retry > MAX_RETRIES:
logging.error('Youtube : No longer attempting to retry.')
exit(1)
max_sleep = 2 ** retry
sleep_seconds = random.random() * max_sleep
logging.warning('Youtube : Sleeping %f seconds and then retrying...'
% sleep_seconds)
time.sleep(sleep_seconds)
def run(options):
youtube = get_authenticated_service()
try:
initialize_upload(youtube, options)
except HttpError as e:
logging.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status,
e.content))

+ 0
- 24
nfo_example.txt View File

@ -1,24 +0,0 @@
### This NFO example show how to construct a NFO for your video ###
### All fields are optional, but you need at least one fields (otherwise NFO is useless :-p) ###
### See --help for options explanation
### Prismedia will search and use NFO in this order: ###
### 1. file passed in command line through --nfo ###
### 2. file inside video directory named after --name command line option append with .txt ###
### 3. file inside video directory named after --file command line option with .txt extension ###
[video]
name = videoname
description = Your complete video description
Multilines description
should be wrote with a blank space
at the beginning of the line :)
tags = list of tags, comma separated
mt = True
category = Films
cca = True
privacy = private
disable-comments = True
thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
nsfw = True
platform = youtube, peertube
language = French
publishAt=2034-05-07T19:00:00

+ 577
- 0
poetry.lock View File

@ -0,0 +1,577 @@
[[package]]
name = "args"
version = "0.1.0"
description = "Command Arguments for Humans."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "cachetools"
version = "4.2.0"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.5"
[[package]]
name = "certifi"
version = "2020.12.5"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "clint"
version = "0.5.1"
description = "Python Command Line Interface Tools"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
args = "*"
[[package]]
name = "configparser"
version = "3.8.1"
description = "Updated configparser from Python 3.7 for Python 2.6+."
category = "main"
optional = false
python-versions = ">=2.6"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8"]
[[package]]
name = "contextlib2"
version = "0.6.0.post1"
description = "Backports and enhancements for the contextlib module"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "future"
version = "0.17.1"
description = "Clean single-source support for Python 3 and 2"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "google-api-core"
version = "1.25.0"
description = "Google API client core library"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
[package.dependencies]
google-auth = ">=1.21.1,<2.0dev"
googleapis-common-protos = ">=1.6.0,<2.0dev"
protobuf = ">=3.12.0"
pytz = "*"
requests = ">=2.18.0,<3.0.0dev"
six = ">=1.13.0"
[package.extras]
grpc = ["grpcio (>=1.29.0,<2.0dev)"]
grpcgcp = ["grpcio-gcp (>=0.2.2)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
[[package]]
name = "google-api-python-client"
version = "1.12.2"
description = "Google API Client Library for Python"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[package.dependencies]
google-api-core = ">=1.21.0,<2dev"
google-auth = ">=1.16.0"
google-auth-httplib2 = ">=0.0.3"
httplib2 = ">=0.9.2,<1dev"
six = ">=1.13.0,<2dev"
uritemplate = ">=3.0.0,<4dev"
[[package]]
name = "google-auth"
version = "1.24.0"
description = "Google Authentication Library"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
[package.dependencies]
cachetools = ">=2.0.0,<5.0"
pyasn1-modules = ">=0.2.1"
rsa = [
{version = "<4.6", markers = "python_version < \"3.6\""},
{version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""},
]
six = ">=1.9.0"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"]
[[package]]
name = "google-auth-httplib2"
version = "0.0.4"
description = "Google Authentication Library: httplib2 transport"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
google-auth = "*"
httplib2 = ">=0.9.1"
six = "*"
[[package]]
name = "google-auth-oauthlib"
version = "0.4.2"
description = "Google Authentication Library"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
google-auth = "*"
requests-oauthlib = ">=0.7.0"
[package.extras]
tool = ["click"]
[[package]]
name = "googleapis-common-protos"
version = "1.52.0"
description = "Common protobufs used in Google APIs"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[package.dependencies]
protobuf = ">=3.6.0"
[package.extras]
grpc = ["grpcio (>=1.0.0)"]
[[package]]
name = "httplib2"
version = "0.12.3"
description = "A comprehensive HTTP client library."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "idna"
version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "oauthlib"
version = "2.1.0"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
category = "main"
optional = false
python-versions = "*"
[package.extras]
rsa = ["cryptography"]
signals = ["blinker"]
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
test = ["nose", "unittest2", "cryptography", "mock", "pyjwt (>=1.0.0)", "blinker"]
[[package]]
name = "protobuf"
version = "3.14.0"
description = "Protocol Buffers"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
six = ">=1.9"
[[package]]
name = "pyasn1"
version = "0.4.8"
description = "ASN.1 types and codecs"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyasn1-modules"
version = "0.2.8"
description = "A collection of ASN.1-based protocols modules."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
name = "python-magic"
version = "0.4.20"
description = "File type identification using libmagic"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "python-magic-bin"
version = "0.4.14"
description = "File type identification using libmagic binary package"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pytz"
version = "2020.5"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "requests"
version = "2.25.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "requests-oauthlib"
version = "0.8.0"
description = "OAuthlib authentication support for Requests."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
oauthlib = ">=0.6.2"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[rsa] (>=0.6.2)", "requests (>=2.0.0)"]
[[package]]
name = "requests-toolbelt"
version = "0.9.1"
description = "A utility belt for advanced users of python-requests"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
name = "rsa"
version = "4.5"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "rsa"
version = "4.7"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=3.5, <4"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "schema"
version = "0.7.3"
description = "Simple data validation library"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
contextlib2 = ">=0.5.5"
[[package]]
name = "six"
version = "1.15.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tzlocal"
version = "1.5.1"
description = "tzinfo object for the local timezone"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pytz = "*"
[[package]]
name = "unidecode"
version = "1.1.2"
description = "ASCII transliterations of Unicode text"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "uritemplate"
version = "3.0.1"
description = "URI templates"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "urllib3"
version = "1.26.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "yapsy"
version = "1.12.2"
description = "Yet another plugin system"
category = "main"
optional = false
python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = ">=3.5"
content-hash = "111e189319a1806c4ccf078c97a6a58738985f31a70a64a9bfc153db7b132008"
[metadata.files]
args = [
{file = "args-0.1.0.tar.gz", hash = "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814"},
]
cachetools = [
{file = "cachetools-4.2.0-py3-none-any.whl", hash = "sha256:c6b07a6ded8c78bf36730b3dc452dfff7d95f2a12a2fed856b1a0cb13ca78c61"},
{file = "cachetools-4.2.0.tar.gz", hash = "sha256:3796e1de094f0eaca982441c92ce96c68c89cced4cd97721ab297ea4b16db90e"},
]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
clint = [
{file = "clint-0.5.1.tar.gz", hash = "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa"},
]
configparser = [
{file = "configparser-3.8.1-py2.py3-none-any.whl", hash = "sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18"},
{file = "configparser-3.8.1.tar.gz", hash = "sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17"},
]
contextlib2 = [
{file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"},
{file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"},
]
docopt = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
future = [
{file = "future-0.17.1.tar.gz", hash = "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"},
]
google-api-core = [
{file = "google-api-core-1.25.0.tar.gz", hash = "sha256:d967beae8d8acdb88fb2f6f769e2ee0ee813042576a08891bded3b8e234150ae"},
{file = "google_api_core-1.25.0-py2.py3-none-any.whl", hash = "sha256:4656345cba9627ab1290eab51300a6397cc50370d99366133df1ae64b744e1eb"},
]
google-api-python-client = [
{file = "google-api-python-client-1.12.2.tar.gz", hash = "sha256:54a7d330833a2e7b0587446d7e4ae6d0244925a9a8e1dfe878f3f7e06cdedb62"},
{file = "google_api_python_client-1.12.2-py2.py3-none-any.whl", hash = "sha256:05cb331ed1aa15746f606c7e36ea51dbe7c29b1a5df9bbf58140901fe23d7142"},
]
google-auth = [
{file = "google-auth-1.24.0.tar.gz", hash = "sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e"},
{file = "google_auth-1.24.0-py2.py3-none-any.whl", hash = "sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"},
]
google-auth-httplib2 = [
{file = "google-auth-httplib2-0.0.4.tar.gz", hash = "sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39"},
{file = "google_auth_httplib2-0.0.4-py2.py3-none-any.whl", hash = "sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee"},
]
google-auth-oauthlib = [
{file = "google-auth-oauthlib-0.4.2.tar.gz", hash = "sha256:65b65bc39ad8cab15039b35e5898455d3d66296d0584d96fe0e79d67d04c51d9"},
{file = "google_auth_oauthlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:d4d98c831ea21d574699978827490a41b94f05d565c617fe1b420e88f1fc8d8d"},
]
googleapis-common-protos = [
{file = "googleapis-common-protos-1.52.0.tar.gz", hash = "sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351"},
{file = "googleapis_common_protos-1.52.0-py2.py3-none-any.whl", hash = "sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24"},
]
httplib2 = [
{file = "httplib2-0.12.3-py3-none-any.whl", hash = "sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8"},
{file = "httplib2-0.12.3.tar.gz", hash = "sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
oauthlib = [
{file = "oauthlib-2.1.0-py2.py3-none-any.whl", hash = "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"},
{file = "oauthlib-2.1.0.tar.gz", hash = "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162"},
]
protobuf = [
{file = "protobuf-3.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a"},
{file = "protobuf-3.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5"},
{file = "protobuf-3.14.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472"},
{file = "protobuf-3.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142"},
{file = "protobuf-3.14.0-cp35-cp35m-win32.whl", hash = "sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2"},
{file = "protobuf-3.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980"},
{file = "protobuf-3.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2"},
{file = "protobuf-3.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1"},
{file = "protobuf-3.14.0-cp36-cp36m-win32.whl", hash = "sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e"},
{file = "protobuf-3.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836"},
{file = "protobuf-3.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd"},
{file = "protobuf-3.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac"},
{file = "protobuf-3.14.0-cp37-cp37m-win32.whl", hash = "sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d"},
{file = "protobuf-3.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5"},
{file = "protobuf-3.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043"},
{file = "protobuf-3.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00"},
{file = "protobuf-3.14.0-py2.py3-none-any.whl", hash = "sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c"},
{file = "protobuf-3.14.0.tar.gz", hash = "sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pyasn1-modules = [
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
{file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
{file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
{file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
{file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
{file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
{file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
{file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
{file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
{file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
{file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
]
python-magic = [
{file = "python-magic-0.4.20.tar.gz", hash = "sha256:0cc52ccad086c377b9194014e3dbf98d94b194344630172510a6a3e716b47801"},
{file = "python_magic-0.4.20-py2.py3-none-any.whl", hash = "sha256:33ce94d9395aa269a9c5fac10ae124a5fb328ebe248f36efc5a43922edee662e"},
]
python-magic-bin = [
{file = "python_magic_bin-0.4.14-py2.py3-none-macosx_10_6_intel.whl", hash = "sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4"},
{file = "python_magic_bin-0.4.14-py2.py3-none-win32.whl", hash = "sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892"},
{file = "python_magic_bin-0.4.14-py2.py3-none-win_amd64.whl", hash = "sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69"},
]
pytz = [
{file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"},
{file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
requests-oauthlib = [
{file = "requests-oauthlib-0.8.0.tar.gz", hash = "sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468"},
{file = "requests_oauthlib-0.8.0-py2.py3-none-any.whl", hash = "sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca"},
]
requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
rsa = [
{file = "rsa-4.5-py2.py3-none-any.whl", hash = "sha256:35c5b5f6675ac02120036d97cf96f1fde4d49670543db2822ba5015e21a18032"},
{file = "rsa-4.5.tar.gz", hash = "sha256:4d409f5a7d78530a4a2062574c7bd80311bc3af29b364e293aa9b03eea77714f"},
{file = "rsa-4.7-py3-none-any.whl", hash = "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"},
{file = "rsa-4.7.tar.gz", hash = "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4"},
]
schema = [
{file = "schema-0.7.3-py2.py3-none-any.whl", hash = "sha256:c331438b60f634cab5664ab720d3083cc444f924d55269530c36b33e3354276f"},
{file = "schema-0.7.3.tar.gz", hash = "sha256:4cf529318cfd1e844ecbe02f41f7e5aa027463e7403666a52746f31f04f47a5e"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
tzlocal = [
{file = "tzlocal-1.5.1.tar.gz", hash = "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"},
]
unidecode = [
{file = "Unidecode-1.1.2-py2.py3-none-any.whl", hash = "sha256:4c9d15d2f73eb0d2649a151c566901f80a030da1ccb0a2043352e1dbf647586b"},
{file = "Unidecode-1.1.2.tar.gz", hash = "sha256:a039f89014245e0cad8858976293e23501accc9ff5a7bdbc739a14a2b7b85cdc"},
]
uritemplate = [
{file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"},
{file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
]
urllib3 = [
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
]
yapsy = [
{file = "Yapsy-1.12.2-py3.6.egg", hash = "sha256:83891e22db0a74445726981df0ef7818dae595454de9cf10b7ba603d45ccd157"},
{file = "Yapsy-1.12.2.tar.gz", hash = "sha256:d8113d9f9c74eacf65b4663c9c037d278c9cb273b5eee5f0e1803baeedb23f8b"},
]

+ 12
- 0
prismedia/__init__.py View File

@ -0,0 +1,12 @@
from future import standard_library
standard_library.install_aliases()
import logging
logger = logging.getLogger('Prismedia')
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
from . import upload

+ 2
- 0
prismedia/__main__.py View File

@ -0,0 +1,2 @@
from .upload import main
main()

+ 58
- 0
prismedia/configuration.py View File

@ -0,0 +1,58 @@
from configparser import RawConfigParser
from os.path import splitext, basename, dirname, abspath
class Configuration:
"""
Configuration manager that read the configuration from multiples nfo files that are commons for all interfaces
The configuration will be read and overridden in the following order:
NFO.txt -> nfo.txt -> directory_name.txt -> video_file.txt -> video_name.txt
(value in the rightmost file override values in preceding files)
Your interface may add to this list from either side. Refer to the plugin's description.
A plugin can also override completely this configuration by using other means. For example the cli plugins takes
command line arguments with more importance than any nfo file.
Attributes:
CONFIG_FILE Filename for the base configuration file that should be used to set global behavior for
prismedia and its plugins
"""
CONFIG_FILE = "prismedia.config" # TODO: replace with "config.txt"? something else?
def __init__(self):
self.root_path = dirname(abspath(__file__))
self.config_parser = RawConfigParser()
self.configuration_file_list = []
self.base_configuration_file = self.root_path + "/config/" + self.CONFIG_FILE
self.config_parser.read(self.base_configuration_file)
def read_commons_nfo(self, video_path, video_name):
video_directory = dirname(video_path)
directory_name = basename(video_directory)
video_file = splitext(basename(video_path))[0]
self.configuration_file_list.append(video_directory + "/" + "NFO.txt")
self.configuration_file_list.append(video_directory + "/" + "nfo.txt")
self.configuration_file_list.append(video_directory + "/" + directory_name + ".txt")
self.configuration_file_list.append(video_directory + "/" + video_file + ".txt")
if video_name and video_name != video_file:
self.configuration_file_list.append(video_directory + "/" + video_name + ".txt")
self.config_parser.read(self.configuration_file_list)
# Do not use this in actual production ready code. Prismedia should not write any file
# This can be used in local to see how a plugin's variable needs to be written in the global NFO
# I am afraid that trying to save a plugin info will also save the current config for the video
def _write_config(self):
"""
Write the content of the ConfigParser in a file.
"""
# Do not use `assert` since we want to do the opposite and optimize the other way around
if not __debug__:
raise AssertionError("The method `Configuration._write_config` should not be called in production")
with open(self.base_configuration_file + "_generated", "w") as configFile:
self.config_parser.write(configFile)
configuration_instance = Configuration()

+ 254
- 0
prismedia/core.py View File

@ -0,0 +1,254 @@
#!/usr/bin/python
# coding: utf-8
# NOTE: Since we use config file to set some defaults values, it is not possible to use the standard syntax with brackets, we use parenthesis instead.
# If we were to use them we would override configuration file values with default values of cli.
# TODO: change `youtube-at` and `peertube-at` that are not easily expendable as options in my opinion
# TODO: remove `--url-only` and `--batch`
"""
prismedia - tool to upload videos to different platforms (historically Peertube and Youtube)
Usage:
prismedia [cli] [options] --file=<file>
prismedia <interface> [<parameters>...]
prismedia --heartbeat
prismedia --help | -h | --version | -V
Options:
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option except if you provide the name of a plugin interface (see <interface>).
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
--name=NAME Name of the video to upload. (default to video filename)
-d, --description=STRING Description of the video. (default: default description)
-t, --tag=STRING Tags for the video. comma separated.
WARN: tags with punctuation (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
-c, --category=STRING Category for the videos, see below. (default: Films)
--licence=STRING Creative Common licence tag (for example: CC-BY-SA) (default: proprietary)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
--nfo=STRING Configure a specific nfo file to set options for the video.
By default Prismedia search a .txt based on the video name and will
decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded)
See nfo_example.txt for more details
--language=STRING Specify the default language for video. See below for supported language. (default is English)
--publish-at=DATE Publish the video at the given DATE using local server timezone.
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future
--peertube-at=DATE Override publish-at for the corresponding platform. Allow to create preview on specific platform
--youtube-at=DATE Override publish-at for the corresponding platform. Allow to create preview on specific platform
--original-date=DATE Configure the video as initially recorded at DATE
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the past
--auto-original-date Automatically use the file modification time as original date
Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
--channel=STRING Set the channel to use for the video (Peertube only)
If the channel is not found, spawn an error except if --channelCreate is set.
--channel-create Create the channel if not exists. (Peertube only, default do not create)
Only relevant if --channel is set.
--playlist=STRING Set the playlist to use for the video.
If the playlist is not found, spawn an error except if --playlistCreate is set.
--playlist-create Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set.
--progress=STRING Set the progress bar view, one of percentage, bigFile, accurate. [default: percentage]
--heartbeat Use some credits to show some activity for you apikey so the platform know it is used and would not inactivate your keys.
-h, --help Show this help. Note that calling `help` without the `--` calls a plugin showing a different help for the plugins.
-V, --version Show the version.
Plugins options:
<interface> Interface plugin to use to provide the video to upload. Select the interface you want to use. If `--file` is provided instead the interface will be the command line.
--platform=STRING Platforms plugins to use. Usually one platform plugin upload to one platform website (comma separated list) (default: all)
--consumer=STRING Consumers plugins to use. They are executed after an upload has been done (comma separated list) (default: all)
Logging options:
--log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info)
-q, --quiet Suppress any log except Critical (alias for --log=critical).
-u, --url-only Display generated URL after upload directly on stdout, implies --quiet
--batch Display generated URL after upload with platform information for easier parsing. Implies --quiet
Be careful --batch and --url-only are mutually exclusives.
Strict options:
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
tags, thumbnail, ...
All strict option are optionals and are provided only to avoid errors when uploading :-)
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
All strict options are off by default
--with-NFO Prevent the upload without a NFO, either specified via cli or found in the directory
--with-thumbnail Prevent the upload without a thumbnail
--with-name Prevent the upload if no name are found
--with-description Prevent the upload without description
--with-tag Prevent the upload without tags
--with-playlist Prevent the upload if no playlist
--with-publish-at Prevent the upload if no schedule
--with-original-date Prevent the upload if no original date configured
--with-platform Prevent the upload if at least one platform is not specified
--with-category Prevent the upload if no category
--with-language Prevent upload if no language
--with-channel Prevent upload if no channel
Categories:
Category is the type of video you upload. Default is films.
Here are available categories from Peertube and Youtube:
music, films, vehicles, sports, travels, gaming, people,
comedy, entertainment, news, how to, education, activism,
science & technology, science, technology, animals
Languages:
Language of the video (audio track), choose one. Default is English
Here are available languages from Peertube and Youtube:
Arabic, English, French, German, Hindi, Italian, Japanese,
Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
"""
import os
import logging
import pluginInterfaces as pi
import configuration
import utils
import video as vid
from docopt import docopt
from yapsy.PluginManager import PluginManagerSingleton
# logging.basicConfig(level=logging.DEBUG)
VERSION = "prismedia v1.0.0-plugins-alpha"
def loadPlugins():
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
config = configuration.configuration_instance
basePluginsPath = [config.root_path + "/plugins"]
# TODO: check if AutoInstallPluginManager can help install new plugins or if it is already easy enough to download
# and unzip a file.
PluginManagerSingleton.setBehaviour([ConfigurablePluginManager])
pluginManager = PluginManagerSingleton.get()
pluginManager.setPluginPlaces(directories_list=basePluginsPath)
pluginManager.setPluginInfoExtension("prismedia-plugin")
pluginManager.setConfigParser(config.config_parser, pluginManager.config_has_changed)
# Define the various categories corresponding to the different
# kinds of plugins you have defined
pluginManager.setCategoriesFilter({
pi.PluginTypes.ALL: pi.IPrismediaBasePlugin,
pi.PluginTypes.INTERFACE: pi.IInterfacePlugin,
pi.PluginTypes.PLATFORM: pi.IPlatformPlugin,
pi.PluginTypes.CONSUMER: pi.IConsumerPlugin,
})
pluginManager.collectPlugins()
return pluginManager
# TODO: cut this function into smaller ones
def main():
logger = logging.getLogger('Prismedia')
# TODO: Check: Maybe this does not work good when installed via pip.
pluginManager = loadPlugins()
# TODO: add the arguments’s verification (copy/adapt the Schema table)
options = docopt(__doc__, version=VERSION)
# Helper functionalities help the user but do not upload anything
if not utils.helperFunctionalities(options):
exit(os.EX_OK)
# Get all arguments needed by core only before calling any plugin
listPlatforms = utils.getOption(options, "--platform")
listConsumers = utils.getOption(options, "--consumer")
if options["<interface>"]:
interface_name = utils.getOption("<interface>")
else:
interface_name = "cli"
interface = pluginManager.getPluginByName(interface_name, pi.PluginTypes.INTERFACE)
video = vid.Video()
try:
if not interface.plugin_object.prepare_options(video, options):
# The plugin asked to stop execution.
exit(os.EX_OK)
except Exception as e:
logger.critical(utils.get_exception_string(e))
exit(os.EX_CONFIG)
if listPlatforms:
platforms = pluginManager.getPluginsOf(categories=pi.PluginTypes.PLATFORM, name=[listPlatforms.split(",")])
else:
platforms = pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM)
if listConsumers:
consumers = pluginManager.getPluginsOf(categories=pi.PluginTypes.CONSUMER, name=[listConsumers.split(",")])
else:
consumers = pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER)
# Let each plugin check its options before starting any upload
# We cannot merge this loop with the one from interface since the interface can change which plugin to use
# We need to create each platform object in video, so we cannot merge this loop with the following one
for plugin in platforms:
# TODO: Check this is needed or not: in case of no plugin or wrong name maybe the list is empty instead of there being a None value
if plugin is None:
# TODO: log instead to error? critical?
print("No plugin installed name `" + plugin.name + "`.")
exit(os.EX_USAGE)
try:
video.platform[plugin.name] = vid.Platform()
if not plugin.plugin_object.prepare_options(video, options):
# A plugin found ill formed options, it should have logged the precises info
print(plugin.name + " found a malformed option.")
exit(os.EX_CONFIG)
except Exception as e:
logger.critical("Error while preparing plugin `" + plugin.name + "`: " + utils.get_exception_string(e))
exit(os.EX_CONFIG)
for plugin in consumers:
# TODO: Check this is needed or not: in case of no plugin or wrong name maybe the list is empty instead of there being a None value
if plugin is None:
# TODO: log instead to error? critical?
print("No plugin installed name `" + plugin.name + "`.")
exit(os.EX_USAGE)
try:
if not plugin.plugin_object.prepare_options(video, options):
# A plugin found ill formed options, it should have logged the precises info
print(plugin.name + " found a malformed option.")
exit(os.EX_CONFIG)
except Exception as e:
logger.critical(utils.get_exception_string(e))
exit(os.EX_CONFIG)
if video.path == "":
# TODO: log instead to error? critical?
print("No valid path to a video file has been provided.")
exit(os.EX_USAGE)
print("All options validated, starting uploads onto platforms")
for platform in platforms:
print("Uploading to: " + platform.name)
try:
platform.plugin_object.upload(video, options)
except Exception as e: # TODO: Maybe not catch every Exception?
logger.critical(utils.get_exception_string(e))
video.platform[platform.name].error = e
video.platform[platform.name].publishAt = None
video.platform[platform.name].url = None
print("All uploads have been done, calling consumers plugins")
for consumer in consumers:
print("Calling consumer: " + consumer.name)
consumer.plugin_object.finished(video, options)
main()

+ 24
- 0
prismedia/genconfig.py View File

@ -0,0 +1,24 @@
from os.path import join, abspath, isfile, dirname, exists
from os import listdir
from shutil import copyfile
import logging
logger = logging.getLogger('Prismedia')
from . import utils
def genconfig():
path = join(dirname(__file__), 'config')
files = [f for f in listdir(path) if isfile(join(path, f))]
for f in files:
final_f = f.replace(".sample", "")
if exists(final_f) and not utils.ask_overwrite(final_f + " already exists. Do you want to overwrite it?"):
continue
copyfile(join(path, f), final_f)
logger.info(str(final_f) + " correctly generated, you may now edit it to fill your credentials.")
if __name__ == '__main__':
genconfig()

+ 72
- 0
prismedia/pluginInterfaces.py View File

@ -0,0 +1,72 @@
from enum import Enum
from yapsy.IPlugin import IPlugin
class PluginTypes(Enum):
"""Plugin Types possibles to instantiate in this program."""
ALL = "All"
INTERFACE = "Interface"
PLATFORM = "Platform"
CONSUMER = "Consumer"
class IPrismediaBasePlugin(IPlugin):
"""
Base for prismedias plugin.
"""
def prepare_options(self, video, options):
"""
Return a falsy value to exit the program.
- `video`: video object to be uploaded
- `options`: a dictionary of options to be used by Prismedia and other plugins
"""
raise NotImplementedError("`prepare_options` must be reimplemented by %s" % self)
###
# Interface
###
class IInterfacePlugin(IPrismediaBasePlugin):
"""
Interface for the Interface plugin category.
"""
# TODO: Add callback for communicating upload’s progress to the user
###
# Platform
###
class IPlatformPlugin(IPrismediaBasePlugin):
"""
Interface for the Platform plugin category.
"""
def heartbeat(self):
"""
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use.
"""
raise NotImplementedError("`heartbeat` must be reimplemented by %s" % self)
def upload(self, video, options):
"""
The upload function
"""
raise NotImplementedError("`upload` must be reimplemented by %s" % self)
###
# Consumer
###
class IConsumerPlugin(IPrismediaBasePlugin):
"""
Interface for the Consumer plugin category.
"""
def finished(self, video, options):
"""
What to do once the uploads are done.
- `video` is an object containing the video details. The `platforms` key contain a list of the platforms the video has been uploaded to and the status
"""
raise NotImplementedError("`finished` must be reimplemented by %s" % self)

+ 9
- 0
prismedia/plugins/consumers/debug.prismedia-plugin View File

@ -0,0 +1,9 @@
[Core]
Name = debug
Module = debug
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Show status of elements

+ 23
- 0
prismedia/plugins/consumers/debug.py View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
# coding: utf-8
import pluginInterfaces as pi
import utils
import video as vid
class Debug(pi.IConsumerPlugin):
"""
Plugin to help knowing the state of prismedia and its variables.
"""
def prepare_options(self, video, options):
print("Debug plugin prepare_options:")
print("Video: ", video)
print("Options: ", options)
return True
def finished(self, video, options):
print("Debug plugin finished:")
print("Video: ", video)
print("Options: ", options)

+ 10
- 0
prismedia/plugins/interfaces/cli.prismedia-plugin View File

@ -0,0 +1,10 @@
[Core]
Name = cli
Module = cli
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = This interface plugin is used to get the videos details from the Command Line Interface (cli).
To work properly it needs to have the `--file="<path/to/you/vide/file>"` argument

+ 68
- 0
prismedia/plugins/interfaces/cli.py View File

@ -0,0 +1,68 @@
import pluginInterfaces as pi
import utils
import configuration
class Cli(pi.IInterfacePlugin):
"""
This is the default interface plugin. It is used when no interface plugin is specified.
Its core functionality is available as a function call to `prepare_options(video, options)` if you do not need the
Cli object.
This can be useful to let other plugins to rely on the defaults behaviors proposed by this one and extend it.
"""
def prepare_options(self, video, options):
prepare_options(video, options)
def prepare_options(video, options):
# TODO: Add the configuration file from the `--nfo` cli argument
_store_docopt_to_configuration(video, options)
_populate_configuration_into_video(video)
def _store_docopt_to_configuration(options):
items = {}
for key, value in options:
if key.startswith("--"):
options.pop(key)
items[key.strip("- ")] = value
configuration.configuration_instance.config_parser.read_dict(items)
def _populate_configuration_into_video(self, video):
config = configuration.configuration_instance
video.path = utils.getOption(config, "file", video.path)
video.thumbnail = utils.getOption(config, "thumbnail", video.thumbnail)
video.name = utils.getOption(config, "name", video.name)
video.description = utils.getOption(config, "description", video.description)
video.playlistName = utils.getOption(config, "playlist", video.playlistName)
video.privacy = utils.getOption(config, "privacy", video.privacy).lower()
video.category = utils.getOption(config, "category", video.category).lower()
tags = utils.getOption(config, "tag", video.tags)
if isinstance(tags, str):
tags = tags.split(",")
video.tags = tags
video.language = utils.getOption(config, "language", video.language).lower()
video.originalDate = utils.getOption(config, "original-date", video.originalDate)
# TODO: set as an object: { "all": date1, "platformX": date2, … }?
# Maybe the publishAt by platform is better placed in `self.platform`
# And so publishAt would only contains the global date.
video.publishAt = utils.getOption(config, "publish-at", video.publishAt)
# TODO: Add a list of licences
video.licence = utils.getOption(config, "licence", video.licence)
video.disableComments = utils.getOption(config, "disable-comments", video.disableComments)
video.nsfw = utils.getOption(config, "nsfw", video.nsfw)
autoOriginalDate = utils.getOption(config, "auto-original-date", False)
if autoOriginalDate:
# TODO: Implement
raise NotImplementedError("auto-original-date functionality is not yet implemented.")
return video

+ 9
- 0
prismedia/plugins/interfaces/help.prismedia-plugin View File

@ -0,0 +1,9 @@
[Core]
Name = help
Module = help
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Give information about plugins usage by using the syntax `prismedia help [<plugin_name>...]`.

+ 51
- 0
prismedia/plugins/interfaces/help.py View File

@ -0,0 +1,51 @@
import pluginInterfaces as pi
from yapsy.PluginManager import PluginManagerSingleton
class Help(pi.IInterfacePlugin):
"""
The help plugin print the usage information of prismedias plugins.
Use it by simply calling `prismedia help <plugin_name>...`.
For example `prismedia help help` bring this help.
"""
def prepare_options(self, video, options):
print(__name__)
pluginManager = PluginManagerSingleton.get()
if options["<parameters>"]:
parameters = options["<parameters>"]
else:
parameters = ["help"]
for p in parameters:
plugin = pluginManager.getPluginByName(p, pi.PluginTypes.ALL)
if plugin is None:
# TODO: log instead to warning? error?
print("No plugin was found with name:", p)
continue
print(plugin.name + "\t" + plugin.description)
print("Usage:", plugin.plugin_object.__doc__)
# Generic help this plugin is able to give for the
if p == "help":
print("The plugins are stored in the following folders:", pluginManager.getPluginLocator().plugins_places)
print("Category:", pi.PluginTypes.INTERFACE.value)
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.INTERFACE):
print("\t" + plugin.name)
print("Category:", pi.PluginTypes.PLATFORM.value)
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM):
print("\t" + plugin.name)
print("Category:", pi.PluginTypes.CONSUMER.value)
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER):
print("\t" + plugin.name)
# Print a line break between each plugin help.
print()
return False

+ 11
- 0
prismedia/plugins/platforms/peertube.prismedia-plugin View File

@ -0,0 +1,11 @@
[Core]
Name = peertube
Module = peertube
[Documentation]
Author = Le Cygne Noir
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Upload to the peertube platform
**NOT SECURE:** If the peertube instance you want to upload to has no SSL certificate (http but not https), you can set the environment variable `OAUTHLIB_INSECURE_TRANSPORT=1`. Keep in mind that using this option makes your credentials vulnerable to interception by a malicious 3rd party. Use this only with dummy credential on a test instance.

+ 408
- 0
prismedia/plugins/platforms/peertube.py View File

@ -0,0 +1,408 @@
#!/usr/bin/env python
# coding: utf-8
import pluginInterfaces as pi
import utils
import mimetypes
import json
import logging
import datetime
import pytz
from os.path import splitext, basename, abspath
from tzlocal import get_localzone
from configparser import RawConfigParser
from requests_oauthlib import OAuth2Session
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from oauthlib.oauth2 import LegacyApplicationClient
from clint.textui.progress import Bar as ProgressBar
from yapsy.PluginManager import PluginManagerSingleton
logger = logging.getLogger('Prismedia')
upload_finished = False
class Peertube(pi.IPlatformPlugin):
"""
Plugin to upload to the Peertube platform.
The connections files should be set as # TODO: EXPLAIN HOW TO SETUP THE SECRET FILES
- `publish-at-peertube=DATE`: overrides the default `publish-at=DATE` for this platform. # TODO: Maybe we will use a [<plugin_name>] section on the config fire, explain that.
"""
NAME = "peertube" # TODO: find if it is possible to get the plugin’s name from inside the plugin
SECRETS_FILE = "peertube_secret"
PRIVACY = {
"public": 1,
"unlisted": 2,
"private": 3
}
CATEGORY = {
"music": 1,
"films": 2,
"vehicles": 3,
"sport": 5,
"travels": 6,
"gaming": 7,
"people": 8,
"comedy": 9,
"entertainment": 10,
"news": 11,
"how to": 12,
"education": 13,
"activism": 14,
"science & technology": 15,
"science": 15,
"technology": 15,
"animals": 16
}
LANGUAGE = {
"arabic": "ar",
"english": "en",
"french": "fr",
"german": "de",
"hindi": "hi",
"italian": "it",
"japanese": "ja",
"korean": "ko",
"mandarin": "zh",
"portuguese": "pt",
"punjabi": "pa",
"russian": "ru",
"spanish": "es"
}
def __init__(self):
self.channelCreate = False
self.oauth = {}
self.secret = {}
def prepare_options(self, video, options):
pluginManager = PluginManagerSingleton.get()
# TODO: get the `publish-at-peertube=DATE` option
# TODO: get the `channel` and `channel-create` options
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "publish-at", "2034-05-07T19:00:00")
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "channel", "toto")
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "channel-create", False)
video.platform[self.NAME].channel = ""
self.secret = RawConfigParser()
self.secret.read(self.SECRETS_FILE)
self.get_authenticated_service()
return True
def get_authenticated_service(self):
instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip("/")
oauth_client = LegacyApplicationClient(
client_id=str(self.secret.get('peertube', 'client_id'))
)
self.oauth = OAuth2Session(client=oauth_client)
self.oauth.fetch_token(
token_url=str(instance_url + '/api/v1/users/token'),
# lower as peertube does not store uppercase for pseudo
username=str(self.secret.get('peertube', 'username').lower()),
password=str(self.secret.get('peertube', 'password')),
client_id=str(self.secret.get('peertube', 'client_id')),
client_secret=str(self.secret.get('peertube', 'client_secret'))
)
def convert_peertube_date(self, date):
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S')
tz = get_localzone()
tz = pytz.timezone(str(tz))
return tz.localize(date).isoformat()
def get_default_channel(self, user_info):
return user_info['videoChannels'][0]['id']
def get_channel_by_name(self, user_info, video):
for channel in user_info["videoChannels"]:
if channel['displayName'] == video.platform[self.NAME].channel:
return channel['id']
def create_channel(self, instance_url, video):
template = ('Peertube: Channel %s does not exist, creating it.')
logger.info(template % (video.platform[self.NAME].channel))
channel_name = utils.cleanString(video.platform[self.NAME].channel)
# Peertube allows 20 chars max for channel name
channel_name = channel_name[:19]
data = '{"name":"' + channel_name + '", \
"displayName":"' + video.platform[self.NAME].channel + '", \
"description":null, \
"support":null}'
headers = {
'Content-Type': "application/json; charset=UTF-8"
}
try:
response = self.oauth.post(instance_url + "/api/v1/video-channels/",
data=data.encode('utf-8'),
headers=headers)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['videoChannel']
return jresponse['id']
if response.status_code == 409:
logger.critical('Peertube: It seems there is a conflict with an existing channel named '
+ channel_name + '.'
' Please beware Peertube internal name is compiled from 20 firsts characters of channel name.'
' Also note that channel name are not case sensitive (no uppercase nor accent)'
' Please check your channel name and retry.')
exit(1)
else:
logger.critical(('Peertube: Creating channel failed with an unexpected response: '
'%s') % response)
exit(1)
def get_default_playlist(self, user_info):
return user_info['videoChannels'][0]['id']
def get_playlist_by_name(self, instance_url, username, video):
start = 0
user_playlists = json.loads(self.oauth.get(
instance_url + "/api/v1/accounts/" + username + "/video-playlists?start=" + str(
start) + "&count=100").content)
total = user_playlists["total"]
data = user_playlists["data"]
# We need to iterate on pagination as peertube returns max 100 playlists (see #41)
while start < total:
for playlist in data:
if playlist['displayName'] == video.playlistName:
return playlist['id']
start = start + 100
user_playlists = json.loads(self.oauth.get(
instance_url + "/api/v1/accounts/" + username + "/video-playlists?start=" + str(
start) + "&count=100").content)
data = user_playlists["data"]
def create_playlist(self, instance_url, video, channel):
template = ('Peertube: Playlist %s does not exist, creating it.')
logger.info(template % (str(video.playlistName)))
# We use files for form-data Content
# see https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file
# None is used to mute "filename" field
files = {'displayName': (None, str(video.playlistName)),
'privacy': (None, "1"),
'description': (None, "null"),
'videoChannelId': (None, str(channel)),
'thumbnailfile': (None, "null")}
try:
response = self.oauth.post(instance_url + "/api/v1/video-playlists/",
files=files)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['videoPlaylist']
return jresponse['id']
else:
logger.critical(('Peertube: Creating the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def set_playlist(self, instance_url, video_id, playlist_id):
logger.info('Peertube: add video to playlist.')
data = '{"videoId":"' + str(video_id) + '"}'
headers = {
'Content-Type': "application/json"
}
try:
response = self.oauth.post(instance_url + "/api/v1/video-playlists/" + str(playlist_id) + "/videos",
data=data,
headers=headers)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
if response is not None:
if response.status_code == 200:
logger.info('Peertube: Video is successfully added to the playlist.')
else:
logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def upload_video(self, video, options):
def get_userinfo(base_url):
return json.loads(self.oauth.get(base_url + "/api/v1/users/me").content)
def get_file(video_path):
mimetypes.init()
return (basename(video_path), open(abspath(video_path), 'rb'),
mimetypes.types_map[splitext(video_path)[1]])
path = video.path
instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip('/')
user_info = get_userinfo(instance_url)
username = str(self.secret.get('peertube', 'username').lower())
# We need to transform fields into tuple to deal with tags as
# MultipartEncoder does not support list refer
# https://github.com/requests/toolbelt/issues/190 and
# https://github.com/requests/toolbelt/issues/205
fields = [
("name", video.name),
("licence", "1"), # TODO: get licence from video object
("description", video.description),
("category", str(self.CATEGORY[video.category])),
("language", str(self.LANGUAGE[video.language])),
("commentsEnabled", "0" if video.disableComments else "1"),
("nsfw", "1" if video.nsfw else "0"),
("videofile", get_file(path))
]
tag_number = 0
for strtag in video.tags:
tag_number = tag_number + 1
# Empty tag crashes Peertube, so skip them
if strtag == "":
continue
# Tag more than 30 chars crashes Peertube, so skip tags
if len(strtag) >= 30:
logger.warning(
"Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag)
logger.warning("Peertube: Meanwhile, this tag will be skipped")
continue
# Peertube supports only 5 tags at the moment
if tag_number > 5:
logger.warning("Peertube: Sorry, Peertube support 5 tags max, additional tag will be skipped")
logger.warning("Peertube: Skipping tag " + strtag)
continue
fields.append(("tags[]", strtag))
# If peertubeAt exists, use instead of publishAt
if video.platform[self.NAME].publishAt:
publishAt = video.platform[self.NAME].publishAt
elif video.publishAt:
publishAt = video.publishAt
if 'publishAt' in locals():
publishAt = convert_peertube_date(publishAt)
fields.append(("scheduleUpdate[updateAt]", publishAt))
fields.append(("scheduleUpdate[privacy]", str(self.PRIVACY["public"])))
fields.append(("privacy", str(self.PRIVACY["private"])))
else:
fields.append(("privacy", str(self.PRIVACY[video.privacy])))
if video.originalDate:
originalDate = convert_peertube_date(video.originalDate)
fields.append(("originallyPublishedAt", originalDate))
if video.thumbnail:
fields.append(("thumbnailfile", get_file(video.thumbnail)))
fields.append(("previewfile", get_file(video.thumbnail)))
if hasattr(video.platform[self.NAME], "channel"): # TODO: Should always be present
channel_id = self.get_channel_by_name(user_info, video)
if not channel_id and self.channelCreate:
channel_id = self.create_channel(instance_url, video)
elif not channel_id:
logger.warning("Peertube: Channel `" + video.platform[
self.NAME].channel + "` is unknown, using default channel.") # TODO: debate if we should have the same message and behavior than playlist: "does not exist, please set --channelCreate"
channel_id = self.get_default_channel(user_info)
else:
channel_id = self.get_default_channel(user_info)
fields.append(("channelId", str(channel_id)))
if video.playlistName:
playlist_id = get_playlist_by_name(instance_url, username, video)
if not playlist_id and video.playlistCreate:
playlist_id = create_playlist(instance_url, video, channel_id)
elif not playlist_id:
logger.critical(
"Peertube: Playlist `" + video.playlistName + "` does not exist, please set --playlistCreate"
" if you want to create it")
exit(1)
encoder = MultipartEncoder(fields)
if options.get('--quiet'):
multipart_data = encoder
else:
progress_callback = self.create_callback(encoder, options.get('--progress'))
multipart_data = MultipartEncoderMonitor(encoder, progress_callback)
headers = {
'Content-Type': multipart_data.content_type
}
response = self.oauth.post(instance_url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['video']
uuid = jresponse['uuid']
video_id = str(jresponse['id'])
logger.info("Peertube: Video was successfully uploaded.")
template_url = "%s/videos/watch/%s"
video.platform[self.NAME].url = template_url % (instance_url, uuid)
logger.info("Peertube: Watch it at " + video.platform[self.NAME].url + ".")
# Upload is successful we may set playlist
if 'playlist_id' in locals():
set_playlist(instance_url, video_id, playlist_id)
else:
logger.critical(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
def create_callback(self, encoder, progress_type):
upload_size_MB = encoder.len * (1 / (1024 * 1024))
if progress_type is None or "percentage" in progress_type.lower():
progress_lambda = lambda x: int((x / encoder.len) * 100) # Default to percentage
elif "bigfile" in progress_type.lower():
progress_lambda = lambda x: x * (1 / (1024 * 1024)) # MB
elif "accurate" in progress_type.lower():
progress_lambda = lambda x: x * (1 / (1024)) # kB
else:
# Should not happen outside of development when adding partly a progress type
logger.critical("Peertube: Unknown progress type `" + progress_type + "`")
exit(1)
bar = ProgressBar(expected_size=progress_lambda(encoder.len), label=f"Peertube upload progress ({upload_size_MB:.2f}MB) ", filled_char='=')
def callback(monitor):
# We want the condition to capture the varible from the parent scope, not a local variable that is created after
global upload_finished
progress = progress_lambda(monitor.bytes_read)
bar.show(progress)
if monitor.bytes_read == encoder.len:
if not upload_finished:
# We get two time in the callback with both bytes equals, skip the first
upload_finished = True
else:
# Print a blank line to not (partly) override the progress bar
print()
logger.info("Peertube: Upload finish, Processing…")
return callback
def heartbeat(self):
"""
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use.
"""
print("heartbeat for peertube (nothing to do)")
pass
# def run(options):
def upload(self, video, options):
logger.info('Peertube: Uploading video...')
self.upload_video(video, options)

+ 397
- 0
prismedia/plugins/platforms/youtube.py View File

@ -0,0 +1,397 @@
#!/usr/bin/env python
# coding: utf-8
# From Youtube samples: https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
import http.client
import httplib2
import random
import time
import copy
import json
from os.path import splitext, basename, exists
import os
import google.oauth2.credentials
import datetime
import pytz
import logging
from tzlocal import get_localzone
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
from google_auth_oauthlib.flow import InstalledAppFlow
from . import utils
logger = logging.getLogger('Prismedia')
# Explicitly tell the underlying HTTP transport library not to retry, since
# we are handling retry logic ourselves.
httplib2.RETRIES = 1
# Maximum number of times to retry before giving up.
MAX_RETRIES = 10
# Youtube retriables cases
RETRIABLE_EXCEPTIONS = (
IOError,
httplib2.HttpLib2Error,
http.client.NotConnected,
http.client.IncompleteRead,
http.client.ImproperConnectionState,
http.client.CannotSendRequest,
http.client.CannotSendHeader,
http.client.ResponseNotReady,
http.client.BadStatusLine,
)
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
CLIENT_SECRETS_FILE = 'youtube_secret.json'
CREDENTIALS_PATH = ".youtube_credentials.json"
SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
CATEGORY = {
"music": 10,
"films": 1,
"vehicles": 2,
"sport": 17,
"travels": 19,
"gaming": 20,
"people": 22,
"comedy": 23,
"entertainment": 24,
"news": 25,
"how to": 26,
"education": 27,
"activism": 29,
"science & technology": 28,
"science": 28,
"technology": 28,
"animals": 15
}
LANGUAGE = {
"arabic": 'ar',
"english": 'en',
"french": 'fr',
"german": 'de',
"hindi": 'hi',
"italian": 'it',
"japanese": 'ja',
"korean": 'ko',
"mandarin": 'zh-CN',
"portuguese": 'pt-PT',
"punjabi": 'pa',
"russian": 'ru',
"spanish": 'es'
}
# Authorize the request and store authorization credentials.
def get_authenticated_service():
check_authenticated_scopes()
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES)
if exists(CREDENTIALS_PATH):
with open(CREDENTIALS_PATH, 'r') as f:
credential_params = json.load(f)
credentials = google.oauth2.credentials.Credentials(
credential_params["token"],
refresh_token=credential_params["_refresh_token"],
token_uri=credential_params["_token_uri"],
client_id=credential_params["_client_id"],
client_secret=credential_params["_client_secret"]
)
else:
credentials = flow.run_console()
with open(CREDENTIALS_PATH, 'w') as f:
p = copy.deepcopy(vars(credentials))
del p["expiry"]
json.dump(p, f)
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False)
def check_authenticated_scopes():
if exists(CREDENTIALS_PATH):
with open(CREDENTIALS_PATH, 'r') as f:
credential_params = json.load(f)
# Check if all scopes are present
if credential_params["_scopes"] != SCOPES:
logger.warning("Youtube: Credentials are obsolete, need to re-authenticate.")
os.remove(CREDENTIALS_PATH)
def convert_youtube_date(date):
# Youtube needs microsecond and the local timezone from ISO 8601
date = date + ".000001"
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f')
tz = get_localzone()
tz = pytz.timezone(str(tz))
return tz.localize(date).isoformat()
def initialize_upload(youtube, options):
path = options.get('--file')
tags = None
if options.get('--tags'):
tags = options.get('--tags').split(',')
category = None
if options.get('--category'):
category = CATEGORY[options.get('--category').lower()]
language = None
if options.get('--language'):
language = LANGUAGE[options.get('--language').lower()]
license = None
if options.get('--cca'):
license = "creativeCommon"
# We set recordingDetails empty because it's easier to add options if it already exists
# and if empty, it does not cause problem during upload
body = {
"snippet": {
"title": options.get('--name'),
"description": options.get('--description') or "default description",
"tags": tags,
# if no category, set default to 1 (Films)
"categoryId": str(category or 1),
"defaultAudioLanguage": str(language or 'en')
},
"status": {
"privacyStatus": str(options.get('--privacy') or "private"),
"license": str(license or "youtube"),
},
"recordingDetails": {
}
}
# If peertubeAt exists, use instead of publishAt
if options.get('--youtubeAt'):
publishAt = options.get('--youtubeAt')
elif options.get('--publishAt'):
publishAt = options.get('--publishAt')
# Check if publishAt variable exists in local variables
if 'publishAt' in locals():
publishAt = convert_youtube_date(publishAt)
body['status']['publishAt'] = str(publishAt)
# Set originalDate except if the user force no originalDate
if options.get('--originalDate'):
originalDate = convert_youtube_date(options.get('--originalDate'))
body['recordingDetails']['recordingDate'] = str(originalDate)
if options.get('--playlist'):
playlist_id = get_playlist_by_name(youtube, options.get('--playlist'))
if not playlist_id and options.get('--playlistCreate'):
playlist_id = create_playlist(youtube, options.get('--playlist'))
elif not playlist_id:
logger.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.")
logger.warning("Youtube: If you want to create it, set the --playlistCreate option.")
playlist_id = ""
else:
playlist_id = ""
# Call the API's videos.insert method to create and upload the video.
insert_request = youtube.videos().insert(
part=','.join(list(body.keys())),
body=body,
media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
)
video_id = resumable_upload(insert_request, 'video', 'insert', options)
# If we get a video_id, upload is successful and we are able to set thumbnail
if video_id and options.get('--thumbnail'):
set_thumbnail(options, youtube, options.get('--thumbnail'), videoId=video_id)
# If we get a video_id and a playlist_id, upload is successful and we are able to set playlist
if video_id and playlist_id != "":
set_playlist(youtube, playlist_id, video_id)
def get_playlist_by_name(youtube, playlist_name):
pageToken = ""
while pageToken != None:
response = youtube.playlists().list(
part='snippet,id',
mine=True,
maxResults=50,
pageToken=pageToken
).execute()
for playlist in response["items"]:
if playlist["snippet"]["title"] == playlist_name:
return playlist["id"]
# Ask next page if there are any
if "nextPageToken" in response:
pageToken = response["nextPageToken"]
else:
pageToken = None
def create_playlist(youtube, playlist_name):
template = 'Youtube: Playlist %s does not exist, creating it.'
logger.info(template % (str(playlist_name)))
resources = build_resource({'snippet.title': playlist_name,
'snippet.description': '',
'status.privacyStatus': 'public'})
response = youtube.playlists().insert(
body=resources,
part='status,snippet,id'
).execute()
return response["id"]
def build_resource(properties):
resource = {}
for p in properties:
# Given a key like "snippet.title", split into "snippet" and "title", where
# "snippet" will be an object and "title" will be a property in that object.
prop_array = p.split('.')
ref = resource
for pa in range(0, len(prop_array)):
is_array = False
key = prop_array[pa]
# For properties that have array values, convert a name like
# "snippet.tags[]" to snippet.tags, and set a flag to handle
# the value as an array.
if key[-2:] == '[]':
key = key[0:len(key)-2:]
is_array = True
if pa == (len(prop_array) - 1):
# Leave properties without values out of inserted resource.
if properties[p]:
if is_array:
ref[key] = properties[p].split(',')
else:
ref[key] = properties[p]
elif key not in ref:
# For example, the property is "snippet.title", but the resource does
# not yet have a "snippet" object. Create the snippet object here.
# Setting "ref = ref[key]" means that in the next time through the
# "for pa in range ..." loop, we will be setting a property in the
# resource's "snippet" object.
ref[key] = {}
ref = ref[key]
else:
# For example, the property is "snippet.description", and the resource
# already has a "snippet" object.
ref = ref[key]
return resource
def set_thumbnail(options, youtube, media_file, **kwargs):
kwargs = utils.remove_empty_kwargs(**kwargs)
request = youtube.thumbnails().set(
media_body=MediaFileUpload(media_file, chunksize=-1,
resumable=True),
**kwargs
)
return resumable_upload(request, 'thumbnail', 'set', options)
def set_playlist(youtube, playlist_id, video_id):
logger.info('Youtube: Configuring playlist...')
resource = build_resource({'snippet.playlistId': playlist_id,
'snippet.resourceId.kind': 'youtube#video',
'snippet.resourceId.videoId': video_id,
'snippet.position': ''}
)
try:
youtube.playlistItems().insert(
body=resource,
part='snippet'
).execute()
except Exception as e:
logger.critical("Youtube: " + utils.get_exception_string(e))
exit(1)
logger.info('Youtube: Video is correctly added to the playlist.')
# This method implements an exponential backoff strategy to resume a
# failed upload.
def resumable_upload(request, resource, method, options):
response = None
error = None
retry = 0
logger_stdout = None
if options.get('--url-only') or options.get('--batch'):
logger_stdout = logging.getLogger('stdoutlogs')
while response is None:
try:
template = 'Youtube: Uploading %s...'
logger.info(template % resource)
status, response = request.next_chunk()
if response is not None:
if method == 'insert' and 'id' in response:
logger.info('Youtube: Video was successfully uploaded.')
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
logger.info(template % response['id'])
template_stdout = 'https://youtu.be/%s'
if options.get('--url-only'):
logger_stdout.info(template_stdout % response['id'])
elif options.get('--batch'):
logger_stdout.info("Youtube: " + template_stdout % response['id'])
return response['id']
elif method != 'insert' or "id" not in response:
logger.info('Youtube: Thumbnail was successfully set.')
else:
template = ('Youtube: The upload failed with an '
'unexpected response: %s')
logger.critical(template % response)
exit(1)
except HttpError as e:
if e.resp.status in RETRIABLE_STATUS_CODES:
template = 'Youtube: A retriable HTTP error %d occurred:\n%s'
error = template % (e.resp.status, e.content)
else:
raise
except RETRIABLE_EXCEPTIONS as e:
error = 'Youtube: A retriable error occurred: %s' % e
if error is not None:
logger.warning(error)
retry += 1
if retry > MAX_RETRIES:
logger.error('Youtube: No longer attempting to retry.')
max_sleep = 2 ** retry
sleep_seconds = random.random() * max_sleep
logger.warning('Youtube: Sleeping %f seconds and then retrying...'
% sleep_seconds)
time.sleep(sleep_seconds)
def heartbeat():
"""Use the minimums credits possibles of the API so google does not readuce to 0 the allowed credits.
This apparently happens after 90 days without any usage of credits.
For more info see the official documentations:
- General informations about quotas: https://developers.google.com/youtube/v3/getting-started#quota
- Quota costs for API requests: https://developers.google.com/youtube/v3/determine_quota_cost
- ToS (Americas) #Usage and Quotas: https://developers.google.com/youtube/terms/api-services-terms-of-service#usage-and-quotas"""
youtube = get_authenticated_service()
try:
get_playlist_by_name(youtube, "Foo")
except HttpError as e:
logger.error('Youtube: An HTTP error %d occurred on heartbeat:\n%s' %
(e.resp.status, e.content))
def run(options):
youtube = get_authenticated_service()
try:
initialize_upload(youtube, options)
except HttpError as e:
logger.error('Youtube: An HTTP error %d occurred:\n%s' % (e.resp.status,
e.content))

+ 12
- 0
prismedia/samples/cli_nfo.txt View File

@ -0,0 +1,12 @@
### This NFO is aimed to be passed to prismedia through the --nfo cli option ###
### eg:
### python -m prismedia --file=/path/to/yourvideo.mp4 --nfo=/path/to/cli_nfo.txt ###
### It's the more priority NFO, only erased by direct cli options ###
[video]
disable-comments = False
nsfw = True
# Publish on Peertube at a specific date
peertubeAt = 2034-05-14T19:00:00
platform = peertube
# debug to display all loaded options
debug = True

+ 26
- 0
prismedia/samples/full_nfo_examples.txt View File

@ -0,0 +1,26 @@
### This NFO example show how to construct a NFO for your video ###
### All fields are optionals, but you need at least one field (otherwise NFO is useless :-p) ###
### See --help for options explanation
[video]
name = videoname
description = Your complete video description
Multilines description
should be wrote with a blank space
at the beginning of the line :-)
tags = list of tags, comma separated
category = Films
cca = True
privacy = private
disable-comments = True
#thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
channel = CookingTest
channelCreate = True
playlist = Desserts Recipes playlist
playlistCreate = True
nsfw = False
platform = youtube, peertube
language = French
publishAt = 2034-05-07T19:00:00
# platformAt overrides the default publishAt for the corresponding platform
#peertubeAt = 2034-05-14T19:00:00
#youtubeAt = 2034-05-21T19:00:00

+ 10
- 0
prismedia/samples/nfo.txt View File

@ -0,0 +1,10 @@
### This NFO is named nfo.txt and is stored in the directory of your videos ###
### This is the less priority NFO, you may use it to set default generic options ###
[video]
# Some generic options for your videos
cca = True
privacy = private
disable-comments = False
channel = DefaultChannel
channelCreate = True
auto-originalDate = True

peertube_secret.sample → prismedia/samples/peertube_secret.sample View File

@ -1,5 +1,5 @@
# This information is obtained upon registration/once logged in a new PeerTube
# on url+'/oauth-clients/local'
# on url+'/api/v1/oauth-clients/local'
# ex: http://domain.example/api/v1/oauth-clients/local
# Warn, no quote " inside this file
[peertube]
@ -8,4 +8,4 @@ client_secret = your_client_secret
username = LecygneNoir
password = your_secure_pwd
peertube_url = https://domain.example
OAUTHLIB_INSECURE_TRANSPORT = '0' #Default use https
OAUTHLIB_INSECURE_TRANSPORT = '0' #Default use https

+ 14
- 0
prismedia/samples/samples.txt View File

@ -0,0 +1,14 @@
### This NFO is named from the directory where your video are. ###
### While more specific than nfo.txt, it's less priority than other NFO ###
### You may use it for options specific to videos in this directory, but still globals ###
[video]
channel = MyMoreSpecificChannel
disable-comments = False
channelCreate = True
category = Films
playlist = Desserts Recipes playlist
playlistCreate = True
nsfw = False
platform = youtube, peertube
language = French
tags = list of tags, comma separated

+ 14
- 0
prismedia/samples/yourvideo.txt View File

@ -0,0 +1,14 @@
### This NFO is named from your video name (here let's say your video is named "yourvideo.mp4") ###
### It aims to give options specific to this videos ###
[video]
disable-comments = False
#thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
name = videoname
description = Your complete video description
Multilines description
should be wrote with a blank space
at the beginning of the line :-)
publishAt = 2034-05-07T19:00:00
# platformAt overrides the default publishAt for the corresponding platform
#peertubeAt = 2034-05-14T19:00:00
#youtubeAt = 2034-05-21T19:00:00

youtube_secret.json.sample → prismedia/samples/youtube_secret.json.sample View File


+ 350
- 0
prismedia/upload.py View File

@ -0,0 +1,350 @@
#!/usr/bin/env python
# coding: utf-8
"""
prismedia - tool to upload videos to Peertube and Youtube
Usage:
prismedia --file=<FILE> [options]
prismedia -f <FILE> --tags=STRING [options]
prismedia --heartbeat
prismedia -h | --help
prismedia --version
Options:
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option.
--name=NAME Name of the video to upload. (default to video filename)
-d, --description=STRING Description of the video. (default: default description)
-t, --tags=STRING Tags for the video. comma separated.
WARN: tags with punctuation (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
-c, --category=STRING Category for the videos, see below. (default: Films)
--cca License should be CreativeCommon Attribution (affects Youtube upload only)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
--nfo=STRING Configure a specific nfo file to set options for the video.
By default Prismedia search a .txt based on the video name and will
decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded)
See nfo_example.txt for more details
--platform=STRING List of platform(s) to upload to, comma separated.
Supported platforms are youtube and peertube (default is both)
--language=STRING Specify the default language for video. See below for supported language. (default is English)
--publishAt=DATE Publish the video at the given DATE using local server timezone.
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future
--peertubeAt=DATE
--youtubeAt=DATE Override publishAt for the corresponding platform. Allow to create preview on specific platform
--originalDate=DATE Configure the video as initially recorded at DATE
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the past
--auto-originalDate Automatically use the file modification time as original date
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
--channel=STRING Set the channel to use for the video (Peertube only)
If the channel is not found, spawn an error except if --channelCreate is set.
--channelCreate Create the channel if not exists. (Peertube only, default do not create)
Only relevant if --channel is set.
--playlist=STRING Set the playlist to use for the video.
If the playlist is not found, spawn an error except if --playlistCreate is set.
--playlistCreate Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set.
--progress=STRING Set the progress bar view, one of percentage, bigFile (MB), accurate (KB).
--heartbeat 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)
-h --help Show this help.
--version Show version.
Logging options
-q --quiet Suppress any log except Critical (alias for --log=critical).
--log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info)
-u --url-only Display generated URL after upload directly on stdout, implies --quiet
--batch Display generated URL after upload with platform information for easier parsing. Implies --quiet
Be careful --batch and --url-only are mutually exclusives.
--debug (Deprecated) Alias for --log=debug. Ignored if --log is set
Strict options:
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
tags, thumbnail, ...
All strict option are optionals and are provided only to avoid errors when uploading :-)
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
All strict options are off by default
--withNFO Prevent the upload without a NFO, either specified via cli or found in the directory
--withThumbnail Prevent the upload without a thumbnail
--withName Prevent the upload if no name are found
--withDescription Prevent the upload without description
--withTags Prevent the upload without tags
--withPlaylist Prevent the upload if no playlist
--withPublishAt Prevent the upload if no schedule
--withOriginalDate Prevent the upload if no original date configured
--withPlatform Prevent the upload if at least one platform is not specified
--withCategory Prevent the upload if no category
--withLanguage Prevent upload if no language
--withChannel Prevent upload if no channel
Categories:
Category is the type of video you upload. Default is films.
Here are available categories from Peertube and Youtube:
music, films, vehicles,
sports, travels, gaming, people,
comedy, entertainment, news,
how to, education, activism, science & technology,
science, technology, animals
Languages:
Language of the video (audio track), choose one. Default is English
Here are available languages from Peertube and Youtube:
Arabic, English, French, German, Hindi, Italian,
Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
"""
import sys
if sys.version_info[0] < 3:
raise Exception("Python 3 or a more recent version is required.")
import os
import datetime
import logging
from docopt import docopt
from . import yt_upload
from . import pt_upload
from . import utils
logger = logging.getLogger('Prismedia')
try:
# noinspection PyUnresolvedReferences
from schema import Schema, And, Or, Optional, SchemaError, Hook, Use
except ImportError:
logger.critical('This program requires that the `schema` data-validation library'
' is installed: \n'
'see https://github.com/halst/schema\n')
exit(1)
try:
# noinspection PyUnresolvedReferences
import magic
except ImportError:
logger.critical('This program requires that the `python-magic` library'
' is installed, NOT the Python bindings to libmagic API \n'
'see https://github.com/ahupp/python-magic\n')
exit(1)
VERSION = "prismedia v0.11.0"
def _optionnalOrStrict(key, scope, error):
option = key.replace('-', '')
option = option[0].upper() + option[1:]
if scope["--with" + option] is True and scope[key] is None:
logger.critical("Prismedia: you have required the strict presence of " + key + " but none is found")
exit(1)
return True
def configureLogs(options):
if options.get('--batch') and options.get('--url-only'):
logger.critical("Prismedia: Please use either --batch OR --url-only, not both.")
exit(1)
# batch and url-only implies quiet
if options.get('--batch') or options.get('--url-only'):
options['--quiet'] = True
if options.get('--quiet'):
# We need to set both log level in the same time
logger.setLevel(50)
ch.setLevel(50)
elif options.get('--log'):
numeric_level = getattr(logging, options["--log"], None)
# We need to set both log level in the same time
logger.setLevel(numeric_level)
ch.setLevel(numeric_level)
elif options.get('--debug'):
logger.warning("DEPRECATION: --debug is deprecated, please use --log=debug instead")
logger.setLevel(10)
ch.setLevel(10)
def configureStdoutLogs():
logger_stdout = logging.getLogger('stdoutlogs')
logger_stdout.setLevel(logging.INFO)
ch_stdout = logging.StreamHandler(stream=sys.stdout)
ch_stdout.setLevel(logging.INFO)
# Default stdout logs is url only
formatter_stdout = logging.Formatter('%(message)s')
ch_stdout.setFormatter(formatter_stdout)
logger_stdout.addHandler(ch_stdout)
def main():
options = docopt(__doc__, version=VERSION)
earlyoptionSchema = Schema({
Optional('--log'): Or(None, And(
str,
Use(str.upper),
validateLogLevel,
error="Log level not recognized")
),
Optional('--quiet', default=False): bool,
Optional('--debug'): bool,
Optional('--url-only', default=False): bool,
Optional('--batch', default=False): bool,
Optional('--withNFO', default=False): bool,
Optional('--withThumbnail', default=False): bool,
Optional('--withName', default=False): bool,
Optional('--withDescription', default=False): bool,
Optional('--withTags', default=False): bool,
Optional('--withPlaylist', default=False): bool,
Optional('--withPublishAt', default=False): bool,
Optional('--withOriginalDate', default=False): bool,
Optional('--withPlatform', default=False): bool,
Optional('--withCategory', default=False): bool,
Optional('--withLanguage', default=False): bool,
Optional('--withChannel', default=False): bool,
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
object: object
})
schema = Schema({
'--file': And(str, os.path.exists, validateVideo, error='file is not supported, please use mp4'),
# Strict option checks - at the moment Schema needs to check Hook and Optional separately #
Hook('--name', handler=_optionnalOrStrict): object,
Hook('--description', handler=_optionnalOrStrict): object,
Hook('--tags', handler=_optionnalOrStrict): object,
Hook('--category', handler=_optionnalOrStrict): object,
Hook('--language', handler=_optionnalOrStrict): object,
Hook('--platform', handler=_optionnalOrStrict): object,
Hook('--publishAt', handler=_optionnalOrStrict): object,
Hook('--originalDate', handler=_optionnalOrStrict): object,
Hook('--thumbnail', handler=_optionnalOrStrict): object,
Hook('--channel', handler=_optionnalOrStrict): object,
Hook('--playlist', handler=_optionnalOrStrict): object,
# Validate checks #
Optional('--name'): Or(None, And(
str,
lambda x: not x.isdigit(),
error="The video name should be a string")
),
Optional('--description'): Or(None, And(
str,
lambda x: not x.isdigit(),
error="The video description should be a string")
),
Optional('--tags'): Or(None, And(
str,
lambda x: not x.isdigit(),
error="Tags should be a string")
),
Optional('--category'): Or(None, And(
str,
validateCategory,
error="Category not recognized, please see --help")
),
Optional('--language'): Or(None, And(
str,
validateLanguage,
error="Language not recognized, please see --help")
),
Optional('--privacy'): Or(None, And(
str,
validatePrivacy,
error="Please use recognized privacy between public, unlisted or private")
),
Optional('--nfo'): Or(None, str),
Optional('--platform'): Or(None, And(str, validatePlatform, error="Sorry, upload platform not supported")),
Optional('--publishAt'): Or(None, And(
str,
validatePublishDate,
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
),
Optional('--peertubeAt'): Or(None, And(
str,
validatePublishDate,
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
),
Optional('--youtubeAt'): Or(None, And(
str,
validatePublishDate,
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
),
Optional('--originalDate'): Or(None, And(
str,
validateOriginalDate,
error="Original date should be the form YYYY-MM-DDThh:mm:ss and has to be in the past")
),
Optional('--auto-originalDate'): bool,
Optional('--cca'): bool,
Optional('--disable-comments'): bool,
Optional('--nsfw'): bool,
Optional('--thumbnail'): Or(None, And(
str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'),
),
Optional('--channel'): Or(None, str),
Optional('--channelCreate'): bool,
Optional('--playlist'): Or(None, str),
Optional('--playlistCreate'): bool,
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
'--heartbeat': bool,
'--help': bool,
'--version': bool,
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
object: object
})
if options.get('--heartbeat'):
yt_upload.heartbeat()
exit(0)
# We need to validate early options first as withNFO and logs options should be prioritized
try:
options = earlyoptionSchema.validate(options)
configureLogs(options)
except SchemaError as e:
logger.critical(e)
exit(1)
if options.get('--url-only') or options.get('--batch'):
configureStdoutLogs()
options = utils.parseNFO(options)
# If after loading NFO we still has no original date and --auto-originalDate is enabled,
# then we need to search from the file
# We need to do that before the strict validation in case --withOriginalDate is enabled
if not options.get('--originalDate') and options.get('--auto-originalDate'):
options['--originalDate'] = utils.searchOriginalDate(options)
# Once NFO are loaded, we need to revalidate strict options in case some were in NFO
try:
options = earlyoptionSchema.validate(options)
except SchemaError as e:
logger.critical(e)
exit(1)
if not options.get('--thumbnail'):
options = utils.searchThumbnail(options)
try:
options = schema.validate(options)
except SchemaError as e:
logger.critical(e)
exit(1)
logger.debug("Python " + sys.version)
logger.debug(options)
if options.get('--platform') is None or "peertube" in options.get('--platform'):
pt_upload.run(options)
if options.get('--platform') is None or "youtube" in options.get('--platform'):
yt_upload.run(options)
if __name__ == '__main__':
logger.warning("DEPRECATION: use 'python -m prismedia', not 'python -m prismedia.upload'")
main()

+ 260
- 0
prismedia/utils.py View File

@ -0,0 +1,260 @@
#!/usr/bin/python
# coding: utf-8
from configparser import RawConfigParser, NoOptionError, NoSectionError
from os.path import dirname, splitext, basename, isfile, getmtime, exists
from yapsy.PluginManager import PluginManagerSingleton
import pluginInterfaces as pi
import re
import unidecode
import logging
import datetime
logger = logging.getLogger("Prismedia")
VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
VALID_CATEGORIES = (
"music", "films", "vehicles",
"sports", "travels", "gaming", "people",
"comedy", "entertainment", "news",
"how to", "education", "activism", "science & technology",
"science", "technology", "animals"
)
VALID_LANGUAGES = ("arabic", "english", "french",
"german", "hindi", "italian",
"japanese", "korean", "mandarin",
"portuguese", "punjabi", "russian", "spanish")
VALID_PROGRESS = ("percentage", "bigfile", "accurate")
def helperFunctionalities(options):
pluginManager = PluginManagerSingleton.get()
optionName = "--heartbeat"
if options.get(optionName):
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM):
plugin.plugin_object.heartbeat()
return False
else:
options.pop(optionName)
return True
def get_exception_string(e):
if hasattr(e, "message"):
return str(e.message)
else:
return str(e)
def validateVideo(path):
supported_types = ["video/mp4"]
detected_type = magic.from_file(path, mime=True)
if detected_type not in supported_types:
print("File", path, "detected type is `" + detected_type + "` which is not one of", supported_types)
force_file = ["y", "yes"]
is_forcing = input("Are you sure you selected the correct file? (y/N)")
if is_forcing.lower() not in force_file:
return False
return path
def validateCategory(category):
if category.lower() in VALID_CATEGORIES:
return True
else:
return False
def validatePrivacy(privacy):
if privacy.lower() in VALID_PRIVACY_STATUSES:
return True
else:
return False
# TODO: remove me?
# def validatePlatform(platform):
# for plfrm in platform.split(','):
# if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
# return False
#
# return True
def validateLanguage(language):
if language.lower() in VALID_LANGUAGES:
return True
else:
return False
def validateDate(date):
return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
def validatePublishDate(publishDate):
# Check date format and if date is future
try:
now = datetime.datetime.now()
publishAt = validateDate(publishDate)
if now >= publishAt:
return False
except ValueError:
return False
return True
def validateOriginalDate(originalDate):
# Check date format and if date is past
try:
now = datetime.datetime.now()
originalDate = validateDate(originalDate)
if now <= originalDate:
return False
except ValueError:
return False
return True
def validateThumbnail(thumbnail):
supported_types = ['image/jpg', 'image/jpeg']
if exists(thumbnail) and \
magic.from_file(thumbnail, mime=True) in supported_types:
return thumbnail
else:
return False
def validateLogLevel(loglevel):
numeric_level = getattr(logging, loglevel, None)
if not isinstance(numeric_level, int):
return False
return True
def validateProgress(progress):
for prgs in progress.split(','):
if prgs.lower().replace(" ", "") not in VALID_PROGRESS:
return False
return True
def ask_overwrite(question):
while True:
reply = str(input(question + ' (Yes/[No]): ') or "No").lower().strip()
if reply[:1] == 'y':
return True
if reply[:1] == 'n':
return False
def remove_empty_kwargs(**kwargs):
good_kwargs = {}
if kwargs is not None:
for key, value in kwargs.items():
if value:
good_kwargs[key] = value
return good_kwargs
def searchOriginalDate(options):
fileModificationDate = str(getmtime(options.get('--file'))).split('.')
return datetime.datetime.fromtimestamp(int(fileModificationDate[0])).isoformat()
# # return the nfo as a RawConfigParser object
# def loadNFO(filename):
# try:
# logger.info("Loading " + filename + " as NFO")
# nfo = RawConfigParser()
# nfo.read(filename, encoding='utf-8')
# return nfo
# except Exception as e:
# logger.critical("Problem loading NFO file " + filename + ": " + str(e))
# exit(1)
# return False
#
#
# def parseNFO(options):
# video_directory = dirname(options.get('--file'))
# directory_name = basename(video_directory)
# nfo_txt = False
# nfo_directory = False
# nfo_videoname = False
# nfo_file = False
# nfo_cli = False
#
# if isfile(video_directory + "/" + "nfo.txt"):
# nfo_txt = loadNFO(video_directory + "/" + "nfo.txt")
# elif isfile(video_directory + "/" + "NFO.txt"):
# nfo_txt = loadNFO(video_directory + "/" + "NFO.txt")
#
# if isfile(video_directory + "/" + directory_name + ".txt"):
# nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt")
#
# if options.get('--name'):
# if isfile(video_directory + "/" + options.get('--name')):
# nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt")
#
# video_file = splitext(basename(options.get('--file')))[0]
# if isfile(video_directory + "/" + video_file + ".txt"):
# nfo_file = loadNFO(video_directory + "/" + video_file + ".txt")
#
# if options.get('--nfo'):
# if isfile(options.get('--nfo')):
# nfo_cli = loadNFO(options.get('--nfo'))
# else:
# logger.critical("Given NFO file does not exist, please check your path.")
# exit(1)
#
# # If there is no NFO and strict option is enabled, then stop there
# if options.get('--withNFO'):
# if not isinstance(nfo_cli, RawConfigParser) and \
# not isinstance(nfo_file, RawConfigParser) and \
# not isinstance(nfo_videoname, RawConfigParser) and \
# not isinstance(nfo_directory, RawConfigParser) and \
# not isinstance(nfo_txt, RawConfigParser):
# logger.critical("You have required the strict presence of NFO but none is found, please use a NFO.")
# exit(1)
#
# # We need to load NFO in this exact order to keep the priorities
# # options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt
# for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]:
# if nfo:
# # We need to check all options and replace it with the nfo value if not defined (None or False)
# for key, value in options.items():
# key = key.replace("--", "")
# try:
# # get string options
# if value is None and nfo.get('video', key):
# options['--' + key] = nfo.get('video', key)
# # get boolean options
# elif value is False and nfo.getboolean('video', key):
# options['--' + key] = nfo.getboolean('video', key)
# except NoOptionError:
# continue
# except NoSectionError:
# logger.critical(nfo + " misses section [video], please check syntax of your NFO.")
# exit(1)
# return options
def cleanString(toclean):
toclean = unidecode.unidecode(toclean)
cleaned = re.sub('[^A-Za-z0-9]+', '', toclean)
return cleaned
def getOption(options, optionName, defaultValue=None):
value = options.get(optionName)
options.pop(optionName)
if value is None:
return defaultValue
return value

+ 123
- 0
prismedia/video.py View File

@ -0,0 +1,123 @@
from os.path import dirname, splitext, basename, isfile, normpath, expanduser
class Platform(object):
"""
Store data representing a Platform.
"""
def __init__(self):
self.error = None
self.publishAt = None
self.url = None
def __repr__(self):
return str(self.__dict__)
# TODO: Add container for `with-*` and a `isValid` method to check that all `with-*` options are present
# TODO: We need some list (using enum?) for the commons licences, language, privacy, categories options
class Video(object):
"""
Store data representing a Video.
"""
def __init__(self):
self.path = ""
self.thumbnail = None
self.name = None
self.description = "Video uploaded with Prismedia"
self.playlistName = None
self.playlistCreate = False
self.privacy = "private"
self.category = "films"
self.tags = []
self.language = "english"
self.originalDate = None # TODO: add a method "extract original date"? -> I feal that needs to be done outside of this class
# TODO: set as an object: { "all": date1, "platformX": date2, …}?
# Maybe the publishAt by platform is better placed in `self.platform`
# And so publishAt would only contains the global date.
self.publishAt = None
# TODO: Add a list of licences
self.licence = "proprietary"
self.disableComments = False
self.nsfw = False
# Each platform should insert here the upload state
self.platform = {}
@property
def path(self):
return self._path
@path.setter
def path(self, value):
path = normpath(expanduser(value))
if value == "":
self._path = ""
elif isfile(path):
self._path = path
else:
# TODO: log instead to debug? info?
print("The path `" + value + "` does not point to a video")
self._path = ""
@property
def thumbnail(self):
if self._thumbnail is not None:
return self._thumbnail
else:
result = None
video_directory = dirname(self.path) + "/"
# First, check for thumbnail based on videoname
if isfile(video_directory + self.name + ".jpg"):
result = video_directory + self.name + ".jpg"
elif isfile(video_directory + self.name + ".jpeg"):
result = video_directory + self.name + ".jpeg"
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
# NOTE: This may be a the exact same check from the previous conditions if self._name = None.
# Bus as far as I know it is not recommended to use privates properties even in the same class
# Maybe check if self.name == splitext(basename(self.path))[0]
# Not done since it is early dev for the plugins rewrite
if not result:
video_file = splitext(basename(self.path))[0]
if isfile(video_directory + video_file + ".jpg"):
result = video_directory + video_file + ".jpg"
elif isfile(video_directory + video_file + ".jpeg"):
result = video_directory + video_file + ".jpeg"
# TODO: move to caller. Logging the output is its responsibility
# Display some info after research
# if not result:
# logger.debug("No thumbnail has been found, continuing")
# else:
# logger.info("Using " + result + " as thumbnail")
return result
@thumbnail.setter
def thumbnail(self, value):
self._thumbnail = value
@property
def name(self):
if self._name is not None:
return self._name
else:
return splitext(basename(self.path))[0]
@name.setter
def name(self, value):
self._name = value
def __repr__(self):
result = "{\n"
for key in self.__dict__:
result += "\t'" + key + "': " + str(self.__dict__[key]) + ",\n"
result += "}\n"
return result

+ 0
- 231
prismedia_upload.py View File

@ -1,231 +0,0 @@
#!/usr/bin/python
# coding: utf-8
"""
prismedia_upload - tool to upload videos to Peertube and Youtube
Usage:
prismedia_upload.py --file=<FILE> [options]
prismedia_upload.py --file=<FILE> --tags=STRING [--mt options]
prismedia_upload.py -h | --help
prismedia_upload.py --version
Options:
--name=NAME Name of the video to upload. (default to video filename)
-d, --description=STRING Description of the video. (default: default description)
-t, --tags=STRING Tags for the video. comma separated.
WARN: tags with space and special characters (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
use mastodon compatibility below
--mt Force Mastodon compatibility for tags (drop every incompatible characters inside tags)
This option requires --tags
-c, --category=STRING Category for the videos, see below. (default: Films)
--cca License should be CreativeCommon Attribution (affects Youtube upload only)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
--nfo=STRING Configure a specific nfo file to set options for the video.
By default Prismedia search a .txt based on video name
See nfo_example.txt for more details
--platform=STRING List of platform(s) to upload to, comma separated.
Supported platforms are youtube and peertube (default is both)
--language=STRING Specify the default language for video. See below for supported language. (default is English)
--publishAt=DATE Publish the video at the given DATE using local server timezone.
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future
For Peertube, requires the "atd" and "curl utilities installed on the system
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
-h --help Show this help.
--version Show version.
Categories:
Category is the type of video you upload. Default is films.
Here are available categories from Peertube and Youtube:
music, films, vehicles,
sports, travels, gaming, people,
comedy, entertainment, news,
how to, education, activism, science & technology,
science, technology, animals
Languages:
Language of the video (audio track), choose one. Default is English
Here are available languages from Peertube and Youtube:
Arabic, English, French, German, Hindi, Italian,
Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
"""
from os.path import dirname, realpath
import sys
import datetime
import logging
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
from docopt import docopt
# Allows a relative import from the parent folder
sys.path.insert(0, dirname(realpath(__file__)) + "/lib")
import yt_upload
import pt_upload
import utils
try:
# noinspection PyUnresolvedReferences
from schema import Schema, And, Or, Optional, SchemaError
except ImportError:
logging.error('This program requires that the `schema` data-validation library'
' is installed: \n'
'see https://github.com/halst/schema\n')
exit(1)
try:
# noinspection PyUnresolvedReferences
import magic
except ImportError:
logging.error('This program requires that the `python-magic` library'
' is installed, NOT the Python bindings to libmagic API \n'
'see https://github.com/ahupp/python-magic\n')
exit(1)
VERSION = "prismedia v0.5"
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
VALID_CATEGORIES = (
"music", "films", "vehicles",
"sports", "travels", "gaming", "people",
"comedy", "entertainment", "news",
"how to", "education", "activism", "science & technology",
"science", "technology", "animals"
)
VALID_PLATFORM = ('youtube', 'peertube')
VALID_LANGUAGES = ('arabic', 'english', 'french',
'german', 'hindi', 'italian',
'japanese', 'korean', 'mandarin',
'portuguese', 'punjabi', 'russian', 'spanish')
def validateVideo(path):
supported_types = ['video/mp4']
if magic.from_file(path, mime=True) in supported_types:
return path
else:
return False
def validateCategory(category):
if category.lower() in VALID_CATEGORIES:
return True
else:
return False
def validatePrivacy(privacy):
if privacy.lower() in VALID_PRIVACY_STATUSES:
return True
else:
return False
def validatePlatform(platform):
for plfrm in platform.split(','):
if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
return False
return True
def validateLanguage(language):
if language.lower() in VALID_LANGUAGES:
return True
else:
return False
def validatePublish(publish):
# Check date format and if date is future
try:
now = datetime.datetime.now()
publishAt = datetime.datetime.strptime(publish, '%Y-%m-%dT%H:%M:%S')
if now >= publishAt:
return False
except ValueError:
return False
return True
def validateThumbnail(thumbnail):
supported_types = ['image/jpg', 'image/jpeg']
if magic.from_file(thumbnail, mime=True) in supported_types:
return thumbnail
else:
return False
if __name__ == '__main__':
options = docopt(__doc__, version=VERSION)
schema = Schema({
'--file': And(str, validateVideo, error='file is not supported, please use mp4'),
Optional('--name'): Or(None, And(
str,
lambda x: not x.isdigit(),
error="The video name should be a string")
),
Optional('--description'): Or(None, And(
str,
lambda x: not x.isdigit(),
error="The video name should be a string")
),
Optional('--tags'): Or(None, And(
str,
lambda x: not x.isdigit(),
error="Tags should be a string")
),
Optional('--mt'): bool,
Optional('--category'): Or(None, And(
str,
validateCategory,
error="Category not recognized, please see --help")
),
Optional('--language'): Or(None, And(
str,
validateLanguage,
error="Language not recognized, please see --help")
),
Optional('--privacy'): Or(None, And(
str,
validatePrivacy,
error="Please use recognized privacy between public, unlisted or private")
),
Optional('--nfo'): Or(None, str),
Optional('--platform'): Or(None, And(str, validatePlatform, error="Sorry, upload platform not supported")),
Optional('--publishAt'): Or(None, And(
str,
validatePublish,
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
),
Optional('--cca'): bool,
Optional('--disable-comments'): bool,
Optional('--nsfw'): bool,
Optional('--thumbnail'): Or(None, And(
str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'),
),
'--help': bool,
'--version': bool
})
options = utils.parseNFO(options)
if not options.get('--thumbnail'):
options = utils.searchThumbnail(options)
try:
options = schema.validate(options)
except SchemaError as e:
exit(e)
if options.get('--platform') is None or "youtube" in options.get('--platform'):
yt_upload.run(options)
if options.get('--platform') is None or "peertube" in options.get('--platform'):
pt_upload.run(options)

+ 51
- 0
pyproject.toml View File

@ -0,0 +1,51 @@
[tool.poetry]
name = "prismedia"
version = "0.11.0"
description = "scripting your way to upload videos on peertube and youtube"
authors = [
"LecygneNoir <git@lecygnenoir.info>",
"Rigel Kent <sendmemail@rigelk.eu>",
"Zykino"
]
license = "AGPL-3.0-only"
readme = 'README.md'
repository = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
homepage = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
keywords = ['peertube', 'youtube', 'prismedia']
[tool.poetry.dependencies]
python = ">=3.5"
clint = "^0.5.1"
configparser = "^3.7.1"
docopt = "^0.6.2"
future = "^0.17.1"
google-api-python-client = ">=1.7.6"
google-auth = ">=1.6.1"
google-auth-httplib2 = ">=0.0.3"
google-auth-oauthlib = ">=0.2.0"
httplib2 = "^0.12.1"
oauthlib = "^2.1.0"
python-magic = "^0.4.15"
python-magic-bin = { version = "^0.4.14", markers = "platform_system == 'Windows'" }
requests = "^2.18.4"
requests-oauthlib = "^0.8.0"
requests-toolbelt = "^0.9.1"
schema = ">=0.7.1"
tzlocal = "^1.5.1"
Unidecode = "^1.0.23"
uritemplate = "^3.0.0"
urllib3 = "^1.22"
Yapsy = "^1.12.2"
[tool.poetry.dev-dependencies]
[tool.poetry.scripts]
prismedia = 'prismedia.upload:main'
prismedia-init = 'prismedia.genconfig:genconfig'
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

+ 141
- 0
requirements.txt View File

@ -0,0 +1,141 @@
args==0.1.0 \
--hash=sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814
cachetools==4.2.0; python_version >= "3.5" and python_version < "4.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") \
--hash=sha256:c6b07a6ded8c78bf36730b3dc452dfff7d95f2a12a2fed856b1a0cb13ca78c61 \
--hash=sha256:3796e1de094f0eaca982441c92ce96c68c89cced4cd97721ab297ea4b16db90e
certifi==2020.12.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
clint==0.5.1 \
--hash=sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa
configparser==3.8.1; python_version >= "2.6" \
--hash=sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18 \
--hash=sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17
contextlib2==0.6.0.post1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" \
--hash=sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b \
--hash=sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e
docopt==0.6.2 \
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
future==0.17.1; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") \
--hash=sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8
google-api-core==1.25.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:d967beae8d8acdb88fb2f6f769e2ee0ee813042576a08891bded3b8e234150ae \
--hash=sha256:4656345cba9627ab1290eab51300a6397cc50370d99366133df1ae64b744e1eb
google-api-python-client==1.12.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:54a7d330833a2e7b0587446d7e4ae6d0244925a9a8e1dfe878f3f7e06cdedb62 \
--hash=sha256:05cb331ed1aa15746f606c7e36ea51dbe7c29b1a5df9bbf58140901fe23d7142
google-auth-httplib2==0.0.4 \
--hash=sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39 \
--hash=sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee
google-auth-oauthlib==0.4.2; python_version >= "3.6" \
--hash=sha256:65b65bc39ad8cab15039b35e5898455d3d66296d0584d96fe0e79d67d04c51d9 \
--hash=sha256:d4d98c831ea21d574699978827490a41b94f05d565c617fe1b420e88f1fc8d8d
google-auth==1.24.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \
--hash=sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e \
--hash=sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8
googleapis-common-protos==1.52.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351 \
--hash=sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24
httplib2==0.12.3 \
--hash=sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8 \
--hash=sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600
idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
oauthlib==2.1.0 \
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b \
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162
protobuf==3.14.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a \
--hash=sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5 \
--hash=sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472 \
--hash=sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142 \
--hash=sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2 \
--hash=sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980 \
--hash=sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2 \
--hash=sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1 \
--hash=sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e \
--hash=sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836 \
--hash=sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd \
--hash=sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac \
--hash=sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d \
--hash=sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5 \
--hash=sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043 \
--hash=sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00 \
--hash=sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c \
--hash=sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce
pyasn1-modules==0.2.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
--hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \
--hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \
--hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \
--hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \
--hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \
--hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \
--hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \
--hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \
--hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \
--hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \
--hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \
--hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd
pyasn1==0.4.8; python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "3.6" or python_full_version >= "3.6.0" and python_version < "3.6" \
--hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \
--hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \
--hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \
--hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \
--hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \
--hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \
--hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \
--hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \
--hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \
--hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
python-magic-bin==0.4.14; platform_system == "Windows" \
--hash=sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4 \
--hash=sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892 \
--hash=sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69
python-magic==0.4.20; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:0cc52ccad086c377b9194014e3dbf98d94b194344630172510a6a3e716b47801 \
--hash=sha256:33ce94d9395aa269a9c5fac10ae124a5fb328ebe248f36efc5a43922edee662e
pytz==2020.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4 \
--hash=sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5
requests-oauthlib==0.8.0 \
--hash=sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468 \
--hash=sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca
requests-toolbelt==0.9.1 \
--hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 \
--hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
rsa==4.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \
--hash=sha256:35c5b5f6675ac02120036d97cf96f1fde4d49670543db2822ba5015e21a18032 \
--hash=sha256:4d409f5a7d78530a4a2062574c7bd80311bc3af29b364e293aa9b03eea77714f \
--hash=sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b \
--hash=sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4
schema==0.7.3 \
--hash=sha256:c331438b60f634cab5664ab720d3083cc444f924d55269530c36b33e3354276f \
--hash=sha256:4cf529318cfd1e844ecbe02f41f7e5aa027463e7403666a52746f31f04f47a5e
six==1.15.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259
tzlocal==1.5.1 \
--hash=sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e
unidecode==1.1.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:4c9d15d2f73eb0d2649a151c566901f80a030da1ccb0a2043352e1dbf647586b \
--hash=sha256:a039f89014245e0cad8858976293e23501accc9ff5a7bdbc739a14a2b7b85cdc
uritemplate==3.0.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \
--hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae
urllib3==1.26.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \
--hash=sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473 \
--hash=sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08
yapsy==1.12.2 \
--hash=sha256:83891e22db0a74445726981df0ef7818dae595454de9cf10b7ba603d45ccd157 \
--hash=sha256:d8113d9f9c74eacf65b4663c9c037d278c9cb273b5eee5f0e1803baeedb23f8b

Loading…
Cancel
Save