Browse Source

Merge branch 'feature/python3' into develop

pull/46/head
LecygneNoir 4 years ago
parent
commit
76e379ab97
7 changed files with 113 additions and 82 deletions
  1. +11
    -0
      CHANGELOG.md
  2. +34
    -27
      README.md
  3. +14
    -11
      lib/pt_upload.py
  4. +6
    -21
      lib/utils.py
  5. +11
    -11
      lib/yt_upload.py
  6. +18
    -12
      prismedia_upload.py
  7. +19
    -0
      requirements.txt

+ 11
- 0
CHANGELOG.md View File

@ -1,5 +1,16 @@
# Changelog # Changelog
## vX.X.X
### 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 ## v0.7.1
### Fixes ### Fixes

+ 34
- 27
README.md View File

@ -1,20 +1,28 @@
# Prismedia # Prismedia
A scripting way to upload videos to peertube and youtube written in python2
Scripting your way to upload videos to peertube and youtube. Works with Python 3.5+.
## Dependencies ## Dependencies
Search in your package manager, otherwise use ``pip install --upgrade``
Search in your package manager, or with `pip` use ``pip install -r requirements.txt``
- configparser
- docopt
- future
- google-api-python-client
- google-auth - google-auth
- google-auth-oauthlib
- google-auth-httplib2 - google-auth-httplib2
- google-api-python-client
- docopt
- schema
- google-auth-oauthlib
- httplib2
- oauthlib
- python-magic - python-magic
- python-magic-bin
- python-magic-bin (Windows only)
- requests
- requests-oauthlib
- requests-toolbelt - requests-toolbelt
- schema
- tzlocal - tzlocal
- unidecode
- Unidecode
- uritemplate
- urllib3
## Configuration ## Configuration
@ -39,18 +47,16 @@ Prismedia will try to use this file at each launch, and re-ask for authenticatio
The default youtube_secret.json should allow you to upload some videos. 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: 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.
- 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 ## How To
Currently in heavy development
Support only mp4 for cross compatibility between Youtube and Peertube Support only mp4 for cross compatibility between Youtube and Peertube
Simply upload a video: Simply upload a video:
@ -86,11 +92,11 @@ Use --help to get all available options:
Options: Options:
-f, --file=STRING Path to the video file to upload in mp4 -f, --file=STRING Path to the video file to upload in mp4
--name=NAME Name of the video to upload. (default to video filename) --name=NAME Name of the video to upload. (default to video filename)
--debug Trigger some debug information like options used (default: no)
-d, --description=STRING Description of the video. (default: default description) -d, --description=STRING Description of the video. (default: default description)
-t, --tags=STRING Tags for the video. comma separated. -t, --tags=STRING Tags for the video. comma separated.
WARN: tags with space and special characters (!, ', ", ?, ...)
WARN: tags with punctuation (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube are not supported by Mastodon to be published from Peertube
use mastodon compatibility below
-c, --category=STRING Category for the videos, see below. (default: Films) -c, --category=STRING Category for the videos, see below. (default: Films)
--cca License should be CreativeCommon Attribution (affects Youtube upload only) --cca License should be CreativeCommon Attribution (affects Youtube upload only)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private) -p, --privacy=STRING Choose between public, unlisted or private. (default: private)
@ -106,7 +112,6 @@ Options:
--publishAt=DATE Publish the video at the given DATE using local server timezone. --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 on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future 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. --thumbnail=STRING Path to a file to use as a thumbnail for the video.
Supported types are jpg and jpeg. Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg By default, prismedia search for an image based on video name followed by .jpg or .jpeg
@ -114,7 +119,7 @@ Options:
If the channel is not found, spawn an error except if --channelCreate is set. 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) --channelCreate Create the channel if not exists. (Peertube only, default do not create)
Only relevant if --channel is set. Only relevant if --channel is set.
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
--playlist=STRING Set the playlist to use for the video.
If the playlist is not found, spawn an error except if --playlistCreate is set. 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) --playlistCreate Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set. Only relevant if --playlist is set.
@ -155,16 +160,18 @@ Languages:
- [x] add videos to playlist - [x] add videos to playlist
- [x] create playlist - [x] create playlist
- [x] schedule your video with publishAt - [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] 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] 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] Allow to choose peertube or youtube upload (to resume failed upload for example)
- [ ] 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] Usable on Desktop (Linux and/or Windows and/or MacOS)
## Compatibility ## 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)
- peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3
## Sources ## Sources
inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload) inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Contributors
Thanks to: @Zykino, @meewan, @rigelk 😘

+ 14
- 11
lib/pt_upload.py View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python
# coding: utf-8 # coding: utf-8
import os import os
@ -10,7 +10,7 @@ import pytz
from os.path import splitext, basename, abspath from os.path import splitext, basename, abspath
from tzlocal import get_localzone from tzlocal import get_localzone
from ConfigParser import RawConfigParser
from configparser import RawConfigParser
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import LegacyApplicationClient from oauthlib.oauth2 import LegacyApplicationClient
from requests_toolbelt.multipart.encoder import MultipartEncoder from requests_toolbelt.multipart.encoder import MultipartEncoder
@ -57,7 +57,7 @@ def get_default_channel(user_info):
def get_channel_by_name(user_info, options): def get_channel_by_name(user_info, options):
for channel in user_info["videoChannels"]: for channel in user_info["videoChannels"]:
if channel['displayName'].encode('utf8') == str(options.get('--channel')):
if channel['displayName'] == options.get('--channel'):
return channel['id'] return channel['id']
@ -67,16 +67,17 @@ def create_channel(oauth, url, options):
channel_name = utils.cleanString(str(options.get('--channel'))) channel_name = utils.cleanString(str(options.get('--channel')))
# Peertube allows 20 chars max for channel name # Peertube allows 20 chars max for channel name
channel_name = channel_name[:19] channel_name = channel_name[:19]
data = '{"name":"' + channel_name +'", \
"displayName":"' + str(options.get('--channel')) +'", \
"description":null}'
data = '{"name":"' + channel_name + '", \
"displayName":"' + options.get('--channel') + '", \
"description":null, \
"support":null}'
headers = { headers = {
'Content-Type': "application/json"
'Content-Type': "application/json; charset=UTF-8"
} }
try: try:
response = oauth.post(url + "/api/v1/video-channels/", response = oauth.post(url + "/api/v1/video-channels/",
data=data,
data=data.encode('utf-8'),
headers=headers) headers=headers)
except Exception as e: except Exception as e:
if hasattr(e, 'message'): if hasattr(e, 'message'):
@ -89,8 +90,10 @@ def create_channel(oauth, url, options):
jresponse = jresponse['videoChannel'] jresponse = jresponse['videoChannel']
return jresponse['id'] return jresponse['id']
if response.status_code == 409: if response.status_code == 409:
logging.error('Peertube: Error: It seems there is a conflict with an existing channel, please beware '
'Peertube internal name is compiled from 20 firsts characters of channel name.'
logging.error('Peertube: Error: 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.') ' Please check your channel name and retry.')
exit(1) exit(1)
else: else:
@ -105,7 +108,7 @@ def get_default_playlist(user_info):
def get_playlist_by_name(user_playlists, options): def get_playlist_by_name(user_playlists, options):
for playlist in user_playlists["data"]: for playlist in user_playlists["data"]:
if playlist['displayName'].encode('utf8') == str(options.get('--playlist')):
if playlist['displayName'] == options.get('--playlist'):
return playlist['id'] return playlist['id']

+ 6
- 21
lib/utils.py View File

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
# coding: utf-8 # coding: utf-8
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
from configparser import RawConfigParser, NoOptionError, NoSectionError
from os.path import dirname, splitext, basename, isfile from os.path import dirname, splitext, basename, isfile
import re import re
from os import devnull from os import devnull
@ -102,7 +102,7 @@ def getLanguage(language, platform):
def remove_empty_kwargs(**kwargs): def remove_empty_kwargs(**kwargs):
good_kwargs = {} good_kwargs = {}
if kwargs is not None: if kwargs is not None:
for key, value in kwargs.iteritems():
for key, value in kwargs.items():
if value: if value:
good_kwargs[key] = value good_kwargs[key] = value
return good_kwargs return good_kwargs
@ -132,7 +132,7 @@ def loadNFO(options):
logging.info("Using " + options.get('--nfo') + " as NFO, loading...") logging.info("Using " + options.get('--nfo') + " as NFO, loading...")
if isfile(options.get('--nfo')): if isfile(options.get('--nfo')):
nfo = RawConfigParser() nfo = RawConfigParser()
nfo.read(options.get('--nfo'))
nfo.read(options.get('--nfo'), encoding='utf-8')
return nfo return nfo
else: else:
logging.error("Given NFO file does not exist, please check your path.") logging.error("Given NFO file does not exist, please check your path.")
@ -147,7 +147,7 @@ def loadNFO(options):
try: try:
logging.info("Using " + nfo_file + " as NFO, loading...") logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser() nfo = RawConfigParser()
nfo.read(nfo_file)
nfo.read(nfo_file, encoding='utf-8')
return nfo return nfo
except Exception as e: except Exception as e:
logging.error("Problem with NFO file: " + str(e)) logging.error("Problem with NFO file: " + str(e))
@ -160,7 +160,7 @@ def loadNFO(options):
try: try:
logging.info("Using " + nfo_file + " as NFO, loading...") logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser() nfo = RawConfigParser()
nfo.read(nfo_file)
nfo.read(nfo_file, encoding='utf-8')
return nfo return nfo
except Exception as e: except Exception as e:
logging.error("Problem with nfo file: " + str(e)) logging.error("Problem with nfo file: " + str(e))
@ -172,7 +172,7 @@ def parseNFO(options):
nfo = loadNFO(options) nfo = loadNFO(options)
if nfo: if nfo:
# We need to check all options and replace it with the nfo value if not defined (None or False) # 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():
for key, value in options.items():
key = key.replace("-", "") key = key.replace("-", "")
try: try:
# get string options # get string options
@ -192,22 +192,7 @@ def upcaseFirstLetter(s):
return s[0].upper() + s[1:] return s[0].upper() + s[1:]
def cleanString(toclean): def cleanString(toclean):
toclean = toclean.decode('utf-8')
toclean = unidecode.unidecode(toclean) toclean = unidecode.unidecode(toclean)
cleaned = re.sub('[^A-Za-z0-9]+', '', toclean) cleaned = re.sub('[^A-Za-z0-9]+', '', toclean)
return cleaned return cleaned
def decodeArgumentStrings(options, encoding):
# Python crash when decoding from UTF-8 to UTF-8, so we prevent this
if "utf-8" == encoding.lower():
return;
if options["--name"] is not None:
options["--name"] = options["--name"].decode(encoding)
if options["--description"] is not None:
options["--description"] = options["--description"].decode(encoding)
if options["--tags"] is not None:
options["--tags"] = options["--tags"].decode(encoding)

+ 11
- 11
lib/yt_upload.py View File

@ -1,8 +1,8 @@
#!/usr/bin/env python2
#!/usr/bin/env python
# coding: utf-8 # coding: utf-8
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa # From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
import httplib
import http.client
import httplib2 import httplib2
import random import random
import time import time
@ -38,13 +38,13 @@ MAX_RETRIES = 10
RETRIABLE_EXCEPTIONS = ( RETRIABLE_EXCEPTIONS = (
IOError, IOError,
httplib2.HttpLib2Error, httplib2.HttpLib2Error,
httplib.NotConnected,
httplib.IncompleteRead,
httplib.ImproperConnectionState,
httplib.CannotSendRequest,
httplib.CannotSendHeader,
httplib.ResponseNotReady,
httplib.BadStatusLine,
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] RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
@ -146,7 +146,7 @@ def initialize_upload(youtube, options):
# Call the API's videos.insert method to create and upload the video. # Call the API's videos.insert method to create and upload the video.
insert_request = youtube.videos().insert( insert_request = youtube.videos().insert(
part=','.join(body.keys()),
part=','.join(list(body.keys())),
body=body, body=body,
media_body=MediaFileUpload(path, chunksize=-1, resumable=True) media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
) )
@ -168,7 +168,7 @@ def get_playlist_by_name(youtube, playlist_name):
maxResults=50 maxResults=50
).execute() ).execute()
for playlist in response["items"]: for playlist in response["items"]:
if playlist["snippet"]['title'].encode('utf8') == str(playlist_name):
if playlist["snippet"]['title'] == playlist_name:
return playlist['id'] return playlist['id']

+ 18
- 12
prismedia_upload.py View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
""" """
@ -13,11 +13,11 @@ Usage:
Options: Options:
-f, --file=STRING Path to the video file to upload in mp4 -f, --file=STRING Path to the video file to upload in mp4
--name=NAME Name of the video to upload. (default to video filename) --name=NAME Name of the video to upload. (default to video filename)
--debug Trigger some debug information like options used (default: no)
-d, --description=STRING Description of the video. (default: default description) -d, --description=STRING Description of the video. (default: default description)
-t, --tags=STRING Tags for the video. comma separated. -t, --tags=STRING Tags for the video. comma separated.
WARN: tags with space and special characters (!, ', ", ?, ...)
WARN: tags with punctuation (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube are not supported by Mastodon to be published from Peertube
use mastodon compatibility below
-c, --category=STRING Category for the videos, see below. (default: Films) -c, --category=STRING Category for the videos, see below. (default: Films)
--cca License should be CreativeCommon Attribution (affects Youtube upload only) --cca License should be CreativeCommon Attribution (affects Youtube upload only)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private) -p, --privacy=STRING Choose between public, unlisted or private. (default: private)
@ -33,7 +33,6 @@ Options:
--publishAt=DATE Publish the video at the given DATE using local server timezone. --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 on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future 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. --thumbnail=STRING Path to a file to use as a thumbnail for the video.
Supported types are jpg and jpeg. Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg By default, prismedia search for an image based on video name followed by .jpg or .jpeg
@ -41,7 +40,7 @@ Options:
If the channel is not found, spawn an error except if --channelCreate is set. 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) --channelCreate Create the channel if not exists. (Peertube only, default do not create)
Only relevant if --channel is set. Only relevant if --channel is set.
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
--playlist=STRING Set the playlist to use for the video.
If the playlist is not found, spawn an error except if --playlistCreate is set. 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) --playlistCreate Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set. Only relevant if --playlist is set.
@ -97,6 +96,9 @@ except ImportError:
'see https://github.com/ahupp/python-magic\n') 'see https://github.com/ahupp/python-magic\n')
exit(1) exit(1)
if sys.version_info[0] < 3:
raise Exception("Python 3 or a more recent version is required.")
VERSION = "prismedia v0.7.1" VERSION = "prismedia v0.7.1"
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted') VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
@ -107,7 +109,7 @@ VALID_CATEGORIES = (
"how to", "education", "activism", "science & technology", "how to", "education", "activism", "science & technology",
"science", "technology", "animals" "science", "technology", "animals"
) )
VALID_PLATFORM = ('youtube', 'peertube')
VALID_PLATFORM = ('youtube', 'peertube', 'none')
VALID_LANGUAGES = ('arabic', 'english', 'french', VALID_LANGUAGES = ('arabic', 'english', 'french',
'german', 'hindi', 'italian', 'german', 'hindi', 'italian',
'japanese', 'korean', 'mandarin', 'japanese', 'korean', 'mandarin',
@ -170,17 +172,17 @@ if __name__ == '__main__':
schema = Schema({ schema = Schema({
'--file': And(str, validateVideo, error='file is not supported, please use mp4'), '--file': And(str, validateVideo, error='file is not supported, please use mp4'),
Optional('--name'): Or(None, And( Optional('--name'): Or(None, And(
basestring,
str,
lambda x: not x.isdigit(), lambda x: not x.isdigit(),
error="The video name should be a string") error="The video name should be a string")
), ),
Optional('--description'): Or(None, And( Optional('--description'): Or(None, And(
basestring,
str,
lambda x: not x.isdigit(), lambda x: not x.isdigit(),
error="The video description should be a string") error="The video description should be a string")
), ),
Optional('--tags'): Or(None, And( Optional('--tags'): Or(None, And(
basestring,
str,
lambda x: not x.isdigit(), lambda x: not x.isdigit(),
error="Tags should be a string") error="Tags should be a string")
), ),
@ -206,6 +208,7 @@ if __name__ == '__main__':
validatePublish, validatePublish,
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future") error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
), ),
Optional('--debug'): bool,
Optional('--cca'): bool, Optional('--cca'): bool,
Optional('--disable-comments'): bool, Optional('--disable-comments'): bool,
Optional('--nsfw'): bool, Optional('--nsfw'): bool,
@ -220,7 +223,6 @@ if __name__ == '__main__':
'--version': bool '--version': bool
}) })
utils.decodeArgumentStrings(options, locale.getpreferredencoding())
options = utils.parseNFO(options) options = utils.parseNFO(options)
if not options.get('--thumbnail'): if not options.get('--thumbnail'):
@ -231,7 +233,11 @@ if __name__ == '__main__':
except SchemaError as e: except SchemaError as e:
exit(e) exit(e)
if options.get('--platform') is None or "youtube" in options.get('--platform'):
yt_upload.run(options)
if options.get('--debug'):
print(sys.version)
print(options)
if options.get('--platform') is None or "peertube" in options.get('--platform'): if options.get('--platform') is None or "peertube" in options.get('--platform'):
pt_upload.run(options) pt_upload.run(options)
if options.get('--platform') is None or "youtube" in options.get('--platform'):
yt_upload.run(options)

+ 19
- 0
requirements.txt View File

@ -0,0 +1,19 @@
configparser
docopt
future
google-api-python-client
google-auth
google-auth-httplib2
google-auth-oauthlib
httplib2
oauthlib
python-magic
python-magic-bin; platform_system == "Windows"
requests
requests-oauthlib
requests-toolbelt
schema
tzlocal
Unidecode
uritemplate
urllib3

Loading…
Cancel
Save