Browse Source

Merge branch 'feature/playlist' into develop

develop
LecygneNoir 5 years ago
parent
commit
04514c86e6
6 changed files with 190 additions and 21 deletions
  1. +3
    -2
      README.md
  2. +56
    -7
      lib/pt_upload.py
  3. +9
    -9
      lib/utils.py
  4. +114
    -3
      lib/yt_upload.py
  5. +2
    -0
      nfo_example.txt
  6. +6
    -0
      prismedia_upload.py

+ 3
- 2
README.md View File

@ -151,7 +151,8 @@ Languages:
- [x] set default language - [x] set default language
- [x] thumbnail/preview - [x] thumbnail/preview
- [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4)) - [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 for Peertube
- [x] add videos to playlist for Youtube
- [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)
- [x] Add publishAt option to plan your videos - [x] Add publishAt option to plan your videos
@ -164,4 +165,4 @@ Languages:
If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3! If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
## Sources ## Sources
inspired by [peeror](https://git.drycat.fr/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)

+ 56
- 7
lib/pt_upload.py View File

@ -51,18 +51,59 @@ def get_authenticated_service(secret):
return oauth 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'))))
data = '{"name":"' + utils.cleanString(str(options.get('--playlist'))) +'", \
"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 upload_video(oauth, secret, options):
def get_userinfo(): 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_file(path): def get_file(path):
mimetypes.init() mimetypes.init()
return (basename(path), open(abspath(path), 'rb'), return (basename(path), open(abspath(path), 'rb'),
mimetypes.types_map[splitext(path)[1]]) mimetypes.types_map[splitext(path)[1]])
path = options.get('--file')
url = str(secret.get('peertube', 'peertube_url')).rstrip('/') url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
user_info = get_userinfo()
# We need to transform fields into tuple to deal with tags as # We need to transform fields into tuple to deal with tags as
# MultipartEncoder does not support list refer # MultipartEncoder does not support list refer
@ -73,8 +114,7 @@ def upload_video(oauth, secret, options):
("licence", "1"), ("licence", "1"),
("description", options.get('--description') or "default description"), ("description", options.get('--description') or "default description"),
("nsfw", str(int(options.get('--nsfw')) or "0")), ("nsfw", str(int(options.get('--nsfw')) or "0")),
("channelId", get_userinfo()),
("videofile", get_file(options.get('--file')))
("videofile", get_file(path))
] ]
if options.get('--tags'): if options.get('--tags'):
@ -89,7 +129,7 @@ def upload_video(oauth, secret, options):
exit(1) exit(1)
# If Mastodon compatibility is enabled, clean tags from special characters # If Mastodon compatibility is enabled, clean tags from special characters
if options.get('--mt'): if options.get('--mt'):
strtag = utils.mastodonTag(strtag)
strtag = utils.cleanString(strtag)
fields.append(("tags", strtag)) fields.append(("tags", strtag))
if options.get('--category'): if options.get('--category'):
@ -129,12 +169,22 @@ def upload_video(oauth, secret, options):
fields.append(("thumbnailfile", get_file(options.get('--thumbnail')))) fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
fields.append(("previewfile", 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) multipart_data = MultipartEncoder(fields)
headers = { headers = {
'Content-Type': multipart_data.content_type 'Content-Type': multipart_data.content_type
} }
response = oauth.post(url + "/api/v1/videos/upload", response = oauth.post(url + "/api/v1/videos/upload",
data=multipart_data, data=multipart_data,
headers=headers) headers=headers)
@ -150,7 +200,6 @@ def upload_video(oauth, secret, options):
else: else:
logging.error(('Peertube: The upload failed with an unexpected response: ' logging.error(('Peertube: The upload failed with an unexpected response: '
'%s') % response) '%s') % response)
print(response.json())
exit(1) exit(1)

+ 9
- 9
lib/utils.py View File

@ -193,15 +193,15 @@ def parseNFO(options):
def upcaseFirstLetter(s): def upcaseFirstLetter(s):
return s[0].upper() + s[1:] return s[0].upper() + s[1:]
def mastodonTag(tag):
tags = tag.split(' ')
mtag = ''
for s in tags:
def cleanString(toclean):
toclean = toclean.split(' ')
cleaned = ''
for s in toclean:
if s == '': if s == '':
continue 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

+ 114
- 3
lib/yt_upload.py View File

@ -9,6 +9,7 @@ import time
import copy import copy
import json import json
from os.path import splitext, basename, exists from os.path import splitext, basename, exists
import os
import google.oauth2.credentials import google.oauth2.credentials
import datetime import datetime
import pytz import pytz
@ -51,13 +52,14 @@ RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
CLIENT_SECRETS_FILE = 'youtube_secret.json' CLIENT_SECRETS_FILE = 'youtube_secret.json'
CREDENTIALS_PATH = ".youtube_credentials.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_SERVICE_NAME = 'youtube'
API_VERSION = 'v3' API_VERSION = 'v3'
# Authorize the request and store authorization credentials. # Authorize the request and store authorization credentials.
def get_authenticated_service(): def get_authenticated_service():
check_authenticated_scopes()
flow = InstalledAppFlow.from_client_secrets_file( flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRETS_FILE, SCOPES) CLIENT_SECRETS_FILE, SCOPES)
if exists(CREDENTIALS_PATH): if exists(CREDENTIALS_PATH):
@ -71,7 +73,7 @@ def get_authenticated_service():
client_secret=credential_params["_client_secret"] client_secret=credential_params["_client_secret"]
) )
else: else:
credentials = flow.run_local_server()
credentials = flow.run_console()
with open(CREDENTIALS_PATH, 'w') as f: with open(CREDENTIALS_PATH, 'w') as f:
p = copy.deepcopy(vars(credentials)) p = copy.deepcopy(vars(credentials))
del p["expiry"] del p["expiry"]
@ -79,6 +81,16 @@ def get_authenticated_service():
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False) 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): def initialize_upload(youtube, options):
path = options.get('--file') path = options.get('--file')
tags = None tags = None
@ -121,6 +133,17 @@ def initialize_upload(youtube, options):
publishAt = tz.localize(publishAt).isoformat() publishAt = tz.localize(publishAt).isoformat()
body['status']['publishAt'] = str(publishAt) 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. # 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(body.keys()),
@ -133,9 +156,77 @@ def initialize_upload(youtube, options):
if video_id and options.get('--thumbnail'): if video_id and options.get('--thumbnail'):
set_thumbnail(youtube, options.get('--thumbnail'), videoId=video_id) 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): def set_thumbnail(youtube, media_file, **kwargs):
kwargs = utils.remove_empty_kwargs(**kwargs) # See full sample for function
kwargs = utils.remove_empty_kwargs(**kwargs)
request = youtube.thumbnails().set( request = youtube.thumbnails().set(
media_body=MediaFileUpload(media_file, chunksize=-1, media_body=MediaFileUpload(media_file, chunksize=-1,
resumable=True), resumable=True),
@ -146,6 +237,26 @@ def set_thumbnail(youtube, media_file, **kwargs):
return resumable_upload(request, 'thumbnail', 'set') 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 # This method implements an exponential backoff strategy to resume a
# failed upload. # failed upload.
def resumable_upload(request, resource, method): def resumable_upload(request, resource, method):

+ 2
- 0
nfo_example.txt View File

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

+ 6
- 0
prismedia_upload.py View File

@ -37,6 +37,10 @@ Options:
--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
--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. -h --help Show this help.
--version Show version. --version Show version.
@ -211,6 +215,8 @@ if __name__ == '__main__':
Optional('--thumbnail'): Or(None, And( Optional('--thumbnail'): Or(None, And(
str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'), str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'),
), ),
Optional('--playlist'): Or(None, str),
Optional('--playlistCreate'): bool,
'--help': bool, '--help': bool,
'--version': bool '--version': bool
}) })

Loading…
Cancel
Save