Browse Source

Merge branch 'release/v0.6'

master
LecygneNoir 5 years ago
parent
commit
9aa84aa8b7
8 changed files with 362 additions and 143 deletions
  1. +19
    -3
      CHANGELOG.md
  2. +26
    -23
      README.md
  3. +111
    -36
      lib/pt_upload.py
  4. +35
    -66
      lib/utils.py
  5. +141
    -10
      lib/yt_upload.py
  6. +3
    -0
      nfo_example.txt
  7. +2
    -2
      peertube_secret.sample
  8. +25
    -3
      prismedia_upload.py

+ 19
- 3
CHANGELOG.md View File

@ -1,11 +1,27 @@
# Prismedia v0.5
# Changelog
## Features
## 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 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
### Features
- plan your Peertube videos! Stable release
- Support for Peertube beta4
- More examples in NFO
- Better support for multilines descriptions
## Fix
### Fixes
- Display datetime for output
- plan video only if upload is successful

+ 26
- 23
README.md View File

@ -1,6 +1,6 @@
# Prismedia
A scripting way to upload videos to peertube and youtube
A scripting way to upload videos to peertube and youtube written in python2
## Dependencies
Search in your package manager, otherwise use ``pip install --upgrade``
@ -11,14 +11,10 @@ Search in your package manager, otherwise use ``pip install --upgrade``
- docopt
- schema
- python-magic
- python-magic-bin
- 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/)
## Configuration
Edit peertube_secret and youtube_secret.json with your credentials.
@ -60,15 +56,21 @@ Simply upload a video:
```
./prismedia_upload.py --file="yourvideo.mp4"
```
```
Specify description and tags:
```
```
./prismedia_upload.py --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
```
Provide a thumbnail:
```
./prismedia_upload.py --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
```
Use a NFO file to specify your video options:
@ -80,15 +82,8 @@ Use a NFO file to specify your video options:
Use --help to get all available options:
```
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:
-f, --file=STRING Path to the video file to upload in mp4
--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.
@ -111,7 +106,14 @@ Options:
--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", "curl" and "jq" utilities installed on the system
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
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
If the playlist is not found, spawn an error except if --playlist-create is set.
--playlistCreate Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set.
-h --help Show this help.
--version Show version.
@ -145,12 +147,13 @@ Languages:
- [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
- [ ] thumbnail/preview (YT workflow: upload video, upload thumbnail, add thumbnail to video)
- [ ] 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] thumbnail/preview
- [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
- [x] add videos to playlist for Peertube
- [x] add videos to playlist for Youtube
- [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/))
- [x] Add publishAt option to plan your videos
- [ ] 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
@ -159,5 +162,5 @@ Languages:
If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
## Sources
inspired by [peeror](https://git.drycat.fr/rigelk/Peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Sources
inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)

+ 111
- 36
lib/pt_upload.py View File

@ -1,11 +1,14 @@
#!/usr/bin/python
#!/usr/bin/env python2
# coding: utf-8
import os
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
@ -23,49 +26,98 @@ PEERTUBE_PRIVACY = {
def get_authenticated_service(secret):
peertube_url = str(secret.get('peertube', 'peertube_url'))
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
oauth_client = LegacyApplicationClient(
client_id=str(secret.get('peertube', 'client_id'))
)
oauth = OAuth2Session(client=oauth_client)
oauth.fetch_token(
token_url=peertube_url + '/api/v1/users/token',
# lower as peertube does not store uppecase 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'))
)
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 get_default_playlist(user_info):
return user_info['videoChannels'][0]['id']
def get_playlist_by_name(user_info, options):
for playlist in user_info["videoChannels"]:
if playlist['displayName'] == options.get('--playlist'):
return playlist['id']
def create_playlist(oauth, url, options):
template = ('Peertube: Playlist %s does not exist, creating it.')
logging.info(template % (str(options.get('--playlist'))))
playlist_name = utils.cleanString(str(options.get('--playlist')))
# Peertube allows 20 chars max for playlist name
playlist_name = playlist_name[:19]
data = '{"name":"' + playlist_name +'", \
"displayName":"' + str(options.get('--playlist')) +'", \
"description":null}'
headers = {
'Content-Type': "application/json"
}
try:
response = oauth.post(url + "/api/v1/video-channels/",
data=data,
headers=headers)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Error: " + str(e.message))
else:
logging.error("Error: " + str(e))
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['videoChannel']
return jresponse['id']
else:
logging.error(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
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"])
return json.loads(oauth.get(url+"/api/v1/users/me").content)
def get_videofile(path):
def get_file(path):
mimetypes.init()
return (basename(path), open(abspath(path), 'rb'),
mimetypes.types_map[splitext(path)[1]])
path = options.get('--file')
url = secret.get('peertube', 'peertube_url')
url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
user_info = get_userinfo()
# 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(path))[0]),
("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_videofile(path))
("videofile", get_file(path))
]
if options.get('--tags'):
@ -76,11 +128,11 @@ def upload_video(oauth, secret, options):
continue
# Tag more than 30 chars crashes Peertube, so exit and check tags
if len(strtag) >= 30:
logging.warning("Sorry, Peertube does not support tag with more than 30 characters, please reduce your tag size")
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)
strtag = utils.cleanString(strtag)
fields.append(("tags", strtag))
if options.get('--category'):
@ -95,22 +147,47 @@ def upload_video(oauth, secret, options):
# 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"))
privacy = None
if options.get('--privacy'):
privacy = options.get('--privacy').lower()
if options.get('--publishAt'):
publishAt = options.get('--publishAt')
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S')
tz = get_localzone()
tz = pytz.timezone(str(tz))
publishAt = tz.localize(publishAt).isoformat()
fields.append(("scheduleUpdate[updateAt]", publishAt))
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
else:
fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"])))
if options.get('--thumbnail'):
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
fields.append(("previewfile", get_file(options.get('--thumbnail'))))
if options.get('--playlist'):
playlist_id = get_playlist_by_name(user_info, options)
if not playlist_id and options.get('--playlistCreate'):
playlist_id = create_playlist(oauth, url, options)
elif not playlist_id:
logging.warning("Playlist `" + options.get('--playlist') + "` is unknown, using default playlist.")
playlist_id = get_default_playlist(user_info)
else:
playlist_id = get_default_playlist(user_info)
fields.append(("channelId", str(playlist_id)))
multipart_data = MultipartEncoder(fields)
headers = {
'Content-Type': multipart_data.content_type
}
response = oauth.post(url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
@ -120,13 +197,11 @@ def upload_video(oauth, secret, options):
jresponse = jresponse['video']
uuid = jresponse['uuid']
idvideo = str(jresponse['id'])
template = ('Peertube : Video was successfully uploaded.\n'
'Watch it at %s/videos/watch/%s.')
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: '
logging.error(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
@ -136,16 +211,16 @@ def run(options):
try:
secret.read(PEERTUBE_SECRETS_FILE)
except Exception as e:
logging.error("Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(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 file...')
logging.info('Peertube: Uploading video...')
upload_video(oauth, secret, options)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Error: " + str(e.message))
logging.error("Peertube: Error: " + str(e.message))
else:
logging.error("Error: " + str(e))
logging.error("Peertube: Error: " + str(e))

+ 35
- 66
lib/utils.py View File

@ -98,6 +98,32 @@ def getLanguage(language, platform):
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')) + "/"
@ -117,7 +143,6 @@ def loadNFO(options):
else:
if options.get('--name'):
nfo_file = video_directory + options.get('--name') + ".txt"
print nfo_file
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
@ -168,71 +193,15 @@ def parseNFO(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:
def cleanString(toclean):
toclean = toclean.split(' ')
cleaned = ''
for s in toclean:
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
strtoclean = unicodedata.normalize('NFKD', unicode (s, 'utf-8')).encode('ASCII', 'ignore')
strtoclean = ''.join(e for e in strtoclean if e.isalnum())
strtoclean = upcaseFirstLetter(strtoclean)
cleaned = cleaned + strtoclean
return mtag
return cleaned

+ 141
- 10
lib/yt_upload.py View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python2
# coding: utf-8
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
@ -9,6 +9,7 @@ import time
import copy
import json
from os.path import splitext, basename, exists
import os
import google.oauth2.credentials
import datetime
import pytz
@ -20,6 +21,7 @@ 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)
@ -50,13 +52,14 @@ 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']
SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
# 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):
@ -70,7 +73,7 @@ def get_authenticated_service():
client_secret=credential_params["_client_secret"]
)
else:
credentials = flow.run_local_server()
credentials = flow.run_console()
with open(CREDENTIALS_PATH, 'w') as f:
p = copy.deepcopy(vars(credentials))
del p["expiry"]
@ -78,6 +81,16 @@ def get_authenticated_service():
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:
logging.warning("Youtube: Credentials are obsolete, need to re-authenticate.")
os.remove(CREDENTIALS_PATH)
def initialize_upload(youtube, options):
path = options.get('--file')
tags = None
@ -120,31 +133,149 @@ def initialize_upload(youtube, options):
publishAt = tz.localize(publishAt).isoformat()
body['status']['publishAt'] = str(publishAt)
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:
logging.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.")
logging.warning("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(body.keys()),
body=body,
media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
)
resumable_upload(insert_request)
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)
# If we get a video_id, upload is successful and we are able to set playlist
if video_id and options.get('--playlist'):
set_playlist(youtube, playlist_id, video_id)
def get_playlist_by_name(youtube, playlist_name):
response = youtube.playlists().list(
part='snippet,id',
mine=True,
maxResults=50
).execute()
for playlist in response["items"]:
if playlist["snippet"]['title'] == playlist_name:
return playlist['id']
def create_playlist(youtube, playlist_name):
template = ('Youtube: Playlist %s does not exist, creating it.')
logging.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(youtube, media_file, **kwargs):
kwargs = utils.remove_empty_kwargs(**kwargs)
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')
def set_playlist(youtube, playlist_id, video_id):
logging.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:
if hasattr(e, 'message'):
logging.error("Youtube: Error: " + str(e.message))
else:
logging.error("Youtube: Error: " + str(e))
logging.info('Youtube: Video is correclty added to the playlist.')
# This method implements an exponential backoff strategy to resume a
# failed upload.
def resumable_upload(request):
def resumable_upload(request, resource, method):
response = None
error = None
retry = 0
while response is None:
try:
logging.info('Youtube : Uploading file...')
template = 'Youtube: Uploading %s...'
logging.info(template % resource)
status, response = request.next_chunk()
if response is not None:
if 'id' in response:
template = ('Youtube : Video was successfully '
'uploaded.\n'
'Watch it at https://youtu.be/%s (post-encoding could take some time)')
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')

+ 3
- 0
nfo_example.txt View File

@ -17,6 +17,9 @@ category = Films
cca = True
privacy = private
disable-comments = True
thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
playlist = My Test Playlist
playlistCreate = True
nsfw = True
platform = youtube, peertube
language = French

+ 2
- 2
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

+ 25
- 3
prismedia_upload.py View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python2
# coding: utf-8
"""
@ -6,11 +6,12 @@ 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 -f <FILE> --tags=STRING [--mt options]
prismedia_upload.py -h | --help
prismedia_upload.py --version
Options:
-f, --file=STRING Path to the video file to upload in mp4
--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.
@ -34,6 +35,13 @@ Options:
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
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
If the playlist is not found, spawn an error except if --playlist-create is set.
--playlistCreate Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set.
-h --help Show this help.
--version Show version.
@ -86,7 +94,7 @@ except ImportError:
'see https://github.com/ahupp/python-magic\n')
exit(1)
VERSION = "prismedia v0.5"
VERSION = "prismedia v0.6"
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
VALID_CATEGORIES = (
@ -151,6 +159,12 @@ def validatePublish(publish):
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__':
@ -199,12 +213,20 @@ if __name__ == '__main__':
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('--playlist'): Or(None, str),
Optional('--playlistCreate'): bool,
'--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:

Loading…
Cancel
Save