diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f0d6d..eb4d58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.11.0 + +## Features + - Add the configuration of Original date of Record for Youtube and Peertube (see #50) + ## v0.10.3 ### Fix diff --git a/README.md b/README.md index 02269d0..4139c9f 100644 --- a/README.md +++ b/README.md @@ -92,24 +92,27 @@ If you plan a larger usage, please consider creating your own youtube_secret fil 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)) -Upload a video: +Here are some demonstration of main usage you would like! +Upload a video: ``` prismedia --file="yourvideo.mp4" ``` Specify description and tags: - ``` prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo" ``` Provide a thumbnail: - ``` prismedia --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 does not exist.: +``` +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) @@ -118,90 +121,9 @@ prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt ``` -Use --help to get all available options: - +Take a look at all available options with `--help`! ``` -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 - --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. - -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 - --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 - +prismedia --help ``` ## Enhanced use of NFO diff --git a/prismedia/pt_upload.py b/prismedia/pt_upload.py index 1f35e62..7c779ad 100644 --- a/prismedia/pt_upload.py +++ b/prismedia/pt_upload.py @@ -63,6 +63,13 @@ def get_channel_by_name(user_info, options): return channel['id'] +def convert_peertube_date(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 create_channel(oauth, url, options): template = ('Peertube: Channel %s does not exist, creating it.') logger.info(template % (str(options.get('--channel')))) @@ -255,16 +262,18 @@ def upload_video(oauth, secret, options): publishAt = options.get('--publishAt') if 'publishAt' in locals(): - publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S') - tz = get_localzone() - tz = pytz.timezone(str(tz)) - publishAt = tz.localize(publishAt).isoformat() + publishAt = convert_peertube_date(publishAt) 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"]))) + # Set originalDate except if the user force no originalDate + if options.get('--originalDate'): + originalDate = convert_peertube_date(options.get('--originalDate')) + fields.append(("originallyPublishedAt", originalDate)) + if options.get('--thumbnail'): fields.append(("thumbnailfile", get_file(options.get('--thumbnail')))) fields.append(("previewfile", get_file(options.get('--thumbnail')))) diff --git a/prismedia/upload.py b/prismedia/upload.py index 14b4133..ea907ee 100755 --- a/prismedia/upload.py +++ b/prismedia/upload.py @@ -34,6 +34,10 @@ Options: 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 @@ -71,6 +75,7 @@ Strict options: --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 @@ -190,11 +195,11 @@ def validateLanguage(language): return False -def validatePublish(publish): +def validatePublishDate(publishDate): # Check date format and if date is future try: now = datetime.datetime.now() - publishAt = datetime.datetime.strptime(publish, '%Y-%m-%dT%H:%M:%S') + publishAt = datetime.datetime.strptime(publishDate, '%Y-%m-%dT%H:%M:%S') if now >= publishAt: return False except ValueError: @@ -202,6 +207,18 @@ def validatePublish(publish): return True +def validateOriginalDate(originalDate): + # Check date format and if date is past + try: + now = datetime.datetime.now() + originalDate = datetime.datetime.strptime(originalDate, '%Y-%m-%dT%H:%M:%S') + if now <= originalDate: + return False + except ValueError: + return False + return True + + def validateThumbnail(thumbnail): supported_types = ['image/jpg', 'image/jpeg'] if os.path.exists(thumbnail) and \ @@ -217,6 +234,7 @@ def validateLogLevel(loglevel): return False return True + def _optionnalOrStrict(key, scope, error): option = key.replace('-', '') option = option[0].upper() + option[1:] @@ -280,6 +298,7 @@ def main(): 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, @@ -298,6 +317,7 @@ def main(): 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, @@ -336,19 +356,25 @@ def main(): 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") + 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, - validatePublish, - error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future") + 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, - validatePublish, - error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future") + 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, @@ -377,6 +403,12 @@ def main(): 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) diff --git a/prismedia/utils.py b/prismedia/utils.py index d7f85ad..c16a885 100644 --- a/prismedia/utils.py +++ b/prismedia/utils.py @@ -2,12 +2,11 @@ # coding: utf-8 from configparser import RawConfigParser, NoOptionError, NoSectionError -from os.path import dirname, splitext, basename, isfile +from os.path import dirname, splitext, basename, isfile, getmtime import re -from os import devnull -from subprocess import check_call, CalledProcessError, STDOUT import unidecode import logging +import datetime logger = logging.getLogger('Prismedia') @@ -135,6 +134,11 @@ def searchThumbnail(options): return options +def searchOriginalDate(options): + fileModificationDate = getmtime(options.get('--file')) + return datetime.datetime.fromtimestamp(fileModificationDate).isoformat() + + # return the nfo as a RawConfigParser object def loadNFO(filename): try: diff --git a/prismedia/yt_upload.py b/prismedia/yt_upload.py index 3cfd900..22db640 100644 --- a/prismedia/yt_upload.py +++ b/prismedia/yt_upload.py @@ -89,6 +89,15 @@ def check_authenticated_scopes(): 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 @@ -107,6 +116,8 @@ def initialize_upload(youtube, options): 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') or splitext(basename(path))[0], @@ -119,6 +130,9 @@ def initialize_upload(youtube, options): "status": { "privacyStatus": str(options.get('--privacy') or "private"), "license": str(license or "youtube"), + }, + "recordingDetails": { + } } @@ -128,15 +142,16 @@ def initialize_upload(youtube, options): elif options.get('--publishAt'): publishAt = options.get('--publishAt') + # Check if publishAt variable exists in local variables if 'publishAt' in locals(): - # Youtube needs microsecond and the local timezone from ISO 8601 - publishAt = 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() + 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'):