Browse Source

Merge branch 'release/v0.6'

LecygneNoir 1 year 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 @@
1
-# Prismedia v0.5
1
+# Changelog
2 2
 
3
-## Features
3
+## v0.6
4
+
5
+### Compatibility ###
6
+**Beware**, the first launch of prismedia for youtube will reask for credentials, this is needed for playlists.
7
+
8
+This release is fully compatible with Peertube v1.0.0!
9
+
10
+### Features
11
+ - Add the possibility to upload thumbnail.
12
+ - Add the possibility to configure playlist. (thanks @zykino for Peertube part)
13
+ - Use the API instead of external binaries for publishAt for both Peertube and Youtube. (thanks @zykino)
14
+ - Use the console option to authenticate against youtube for easier use with ssh'ed servers
15
+ - Add -f as an alias for --file for easier upload.
16
+
17
+## v0.5
18
+
19
+### Features
4 20
  - plan your Peertube videos! Stable release
5 21
  - Support for Peertube beta4
6 22
  - More examples in NFO
7 23
  - Better support for multilines descriptions
8 24
 
9
-## Fix
25
+### Fixes
10 26
  - Display datetime for output
11 27
  - plan video only if upload is successful

+ 26
- 23
README.md View File

@@ -1,6 +1,6 @@
1 1
 # Prismedia
2 2
 
3
-A scripting way to upload videos to peertube and youtube
3
+A scripting way to upload videos to peertube and youtube written in python2
4 4
 
5 5
 ## Dependencies
6 6
 Search in your package manager, otherwise use ``pip install --upgrade``
@@ -11,14 +11,10 @@ Search in your package manager, otherwise use ``pip install --upgrade``
11 11
  - docopt
12 12
  - schema
13 13
  - python-magic
14
+ - python-magic-bin
14 15
  - requests-toolbelt
15 16
  - tzlocal
16 17
 
17
-For Peertube and if you want to use the publishAt option, you also need some utilities on you local system
18
- - [atd](https://linux.die.net/man/8/atd) daemon
19
- - [curl](https://linux.die.net/man/1/curl)
20
- - [jq](https://stedolan.github.io/jq/)
21
-
22 18
 ## Configuration
23 19
 
24 20
 Edit peertube_secret and youtube_secret.json with your credentials.
@@ -60,15 +56,21 @@ Simply upload a video:
60 56
 
61 57
 ```
62 58
 ./prismedia_upload.py --file="yourvideo.mp4"
63
-``` 
59
+```
64 60
 
65 61
 
66 62
 Specify description and tags:
67 63
 
68
-``` 
64
+```
69 65
 ./prismedia_upload.py --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
70 66
 ```
71 67
 
68
+Provide a thumbnail:
69
+
70
+```
71
+./prismedia_upload.py --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
72
+```
73
+
72 74
 
73 75
 Use a NFO file to specify your video options:
74 76
 
@@ -80,15 +82,8 @@ Use a NFO file to specify your video options:
80 82
 Use --help to get all available options:
81 83
 
82 84
 ```
83
-prismedia_upload - tool to upload videos to Peertube and Youtube
84
-
85
-Usage:
86
-  prismedia_upload.py --file=<FILE> [options]
87
-  prismedia_upload.py --file=<FILE> --tags=STRING [--mt options]
88
-  prismedia_upload.py -h | --help
89
-  prismedia_upload.py --version
90
-
91 85
 Options:
86
+  -f, --file=STRING Path to the video file to upload in mp4
92 87
   --name=NAME  Name of the video to upload. (default to video filename)
93 88
   -d, --description=STRING  Description of the video. (default: default description)
94 89
   -t, --tags=STRING  Tags for the video. comma separated.
@@ -111,7 +106,14 @@ Options:
111 106
   --publishAt=DATE  Publish the video at the given DATE using local server timezone.
112 107
                     DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
113 108
                     DATE should be in the future
114
-                    For Peertube, requires the "atd", "curl" and "jq" utilities installed on the system
109
+                    For Peertube, requires the "atd" and "curl utilities installed on the system
110
+  --thumbnail=STRING    Path to a file to use as a thumbnail for the video.
111
+                        Supported types are jpg and jpeg.
112
+                        By default, prismedia search for an image based on video name followed by .jpg or .jpeg
113
+  --playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
114
+                    If the playlist is not found, spawn an error except if --playlist-create is set.
115
+  --playlistCreate  Create the playlist if not exists. (default do not create)
116
+                    Only relevant if --playlist is set.
115 117
   -h --help  Show this help.
116 118
   --version  Show version.
117 119
 
@@ -145,12 +147,13 @@ Languages:
145 147
   - [x] enabling/disabling comment (Peertube only as Youtube API does not support it)
146 148
   - [x] nsfw (Peertube only as Youtube API does not support it)
147 149
   - [x] set default language
148
-  - [ ] thumbnail/preview (YT workflow: upload video, upload thumbnail, add thumbnail to video)
149
-  - [ ] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
150
-  - [ ] add videos to playlist (YT & PT workflow: upload video, find playlist id, add video to playlist)
150
+  - [x] thumbnail/preview
151
+  - [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
152
+  - [x] add videos to playlist for Peertube
153
+  - [x] add videos to playlist for Youtube
151 154
 - [x] Use a config file (NFO) file to retrieve videos arguments
152 155
 - [x] Allow to choose peertube or youtube upload (to resume failed upload for example)
153
-- [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/))
156
+- [x] Add publishAt option to plan your videos
154 157
 - [ ] Record and forget: put the video in a directory, and the script uploads it for you
155 158
 - [ ] Usable on Desktop (Linux and/or Windows and/or MacOS)
156 159
 - [ ] Graphical User Interface
@@ -159,5 +162,5 @@ Languages:
159 162
 
160 163
 If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
161 164
 
162
-## Sources 
163
-inspired by [peeror](https://git.drycat.fr/rigelk/Peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
165
+## Sources
166
+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 @@
1
-#!/usr/bin/python
1
+#!/usr/bin/env python2
2 2
 # coding: utf-8
3 3
 
4 4
 import os
5 5
 import mimetypes
6 6
 import json
7 7
 import logging
8
+import datetime
9
+import pytz
8 10
 from os.path import splitext, basename, abspath
11
+from tzlocal import get_localzone
9 12
 
10 13
 from ConfigParser import RawConfigParser
11 14
 from requests_oauthlib import OAuth2Session
@@ -23,49 +26,98 @@ PEERTUBE_PRIVACY = {
23 26
 
24 27
 
25 28
 def get_authenticated_service(secret):
26
-    peertube_url = str(secret.get('peertube', 'peertube_url'))
29
+    peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
27 30
 
28 31
     oauth_client = LegacyApplicationClient(
29 32
         client_id=str(secret.get('peertube', 'client_id'))
30 33
     )
31
-    oauth = OAuth2Session(client=oauth_client)
32
-    oauth.fetch_token(
33
-        token_url=peertube_url + '/api/v1/users/token',
34
-        # lower as peertube does not store uppecase for pseudo
35
-        username=str(secret.get('peertube', 'username').lower()),
36
-        password=str(secret.get('peertube', 'password')),
37
-        client_id=str(secret.get('peertube', 'client_id')),
38
-        client_secret=str(secret.get('peertube', 'client_secret'))
39
-    )
40
-
34
+    try:
35
+        oauth = OAuth2Session(client=oauth_client)
36
+        oauth.fetch_token(
37
+            token_url=str(peertube_url + '/api/v1/users/token'),
38
+            # lower as peertube does not store uppercase for pseudo
39
+            username=str(secret.get('peertube', 'username').lower()),
40
+            password=str(secret.get('peertube', 'password')),
41
+            client_id=str(secret.get('peertube', 'client_id')),
42
+            client_secret=str(secret.get('peertube', 'client_secret'))
43
+        )
44
+    except Exception as e:
45
+        if hasattr(e, 'message'):
46
+            logging.error("Peertube: Error: " + str(e.message))
47
+            exit(1)
48
+        else:
49
+            logging.error("Peertube: Error: " + str(e))
50
+            exit(1)
41 51
     return oauth
42 52
 
43 53
 
54
+def get_default_playlist(user_info):
55
+    return user_info['videoChannels'][0]['id']
56
+
57
+
58
+def get_playlist_by_name(user_info, options):
59
+    for playlist in user_info["videoChannels"]:
60
+        if playlist['displayName'] == options.get('--playlist'):
61
+            return playlist['id']
62
+
63
+
64
+def create_playlist(oauth, url, options):
65
+    template = ('Peertube: Playlist %s does not exist, creating it.')
66
+    logging.info(template % (str(options.get('--playlist'))))
67
+    playlist_name = utils.cleanString(str(options.get('--playlist')))
68
+    # Peertube allows 20 chars max for playlist name
69
+    playlist_name = playlist_name[:19]
70
+    data = '{"name":"' + playlist_name +'", \
71
+            "displayName":"' + str(options.get('--playlist')) +'", \
72
+            "description":null}'
73
+
74
+    headers = {
75
+        'Content-Type': "application/json"
76
+    }
77
+    try:
78
+        response = oauth.post(url + "/api/v1/video-channels/",
79
+                       data=data,
80
+                       headers=headers)
81
+    except Exception as e:
82
+        if hasattr(e, 'message'):
83
+            logging.error("Error: " + str(e.message))
84
+        else:
85
+            logging.error("Error: " + str(e))
86
+    if response is not None:
87
+        if response.status_code == 200:
88
+            jresponse = response.json()
89
+            jresponse = jresponse['videoChannel']
90
+            return jresponse['id']
91
+        else:
92
+            logging.error(('Peertube: The upload failed with an unexpected response: '
93
+                           '%s') % response)
94
+            exit(1)
95
+
96
+
44 97
 def upload_video(oauth, secret, options):
45 98
 
46 99
     def get_userinfo():
47
-        user_info = json.loads(oauth.get(url + "/api/v1/users/me").content)
48
-        return str(user_info["id"])
100
+        return json.loads(oauth.get(url+"/api/v1/users/me").content)
49 101
 
50
-    def get_videofile(path):
102
+    def get_file(path):
51 103
         mimetypes.init()
52 104
         return (basename(path), open(abspath(path), 'rb'),
53 105
                 mimetypes.types_map[splitext(path)[1]])
54 106
 
55 107
     path = options.get('--file')
56
-    url = secret.get('peertube', 'peertube_url')
108
+    url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
109
+    user_info = get_userinfo()
57 110
 
58 111
     # We need to transform fields into tuple to deal with tags as
59 112
     # MultipartEncoder does not support list refer
60 113
     # https://github.com/requests/toolbelt/issues/190 and
61 114
     # https://github.com/requests/toolbelt/issues/205
62 115
     fields = [
63
-        ("name", options.get('--name') or splitext(basename(path))[0]),
116
+        ("name", options.get('--name') or splitext(basename(options.get('--file')))[0]),
64 117
         ("licence", "1"),
65 118
         ("description", options.get('--description')  or "default description"),
66 119
         ("nsfw", str(int(options.get('--nsfw')) or "0")),
67
-        ("channelId", get_userinfo()),
68
-        ("videofile", get_videofile(path))
120
+        ("videofile", get_file(path))
69 121
     ]
70 122
 
71 123
     if options.get('--tags'):
@@ -76,11 +128,11 @@ def upload_video(oauth, secret, options):
76 128
                 continue
77 129
             # Tag more than 30 chars crashes Peertube, so exit and check tags
78 130
             if len(strtag) >= 30:
79
-                logging.warning("Sorry, Peertube does not support tag with more than 30 characters, please reduce your tag size")
131
+                logging.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce your tag size")
80 132
                 exit(1)
81 133
             # If Mastodon compatibility is enabled, clean tags from special characters
82 134
             if options.get('--mt'):
83
-                strtag = utils.mastodonTag(strtag)
135
+                strtag = utils.cleanString(strtag)
84 136
             fields.append(("tags", strtag))
85 137
 
86 138
     if options.get('--category'):
@@ -95,22 +147,47 @@ def upload_video(oauth, secret, options):
95 147
         # if no language, set default to 1 (English)
96 148
         fields.append(("language", "en"))
97 149
 
98
-    if options.get('--privacy'):
99
-        fields.append(("privacy", str(PEERTUBE_PRIVACY[options.get('--privacy').lower()])))
100
-    else:
101
-        fields.append(("privacy", "3"))
102
-
103 150
     if options.get('--disable-comments'):
104 151
         fields.append(("commentsEnabled", "0"))
105 152
     else:
106 153
         fields.append(("commentsEnabled", "1"))
107 154
 
155
+    privacy = None
156
+    if options.get('--privacy'):
157
+        privacy = options.get('--privacy').lower()
158
+
159
+    if options.get('--publishAt'):
160
+        publishAt = options.get('--publishAt')
161
+        publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S')
162
+        tz = get_localzone()
163
+        tz = pytz.timezone(str(tz))
164
+        publishAt = tz.localize(publishAt).isoformat()
165
+        fields.append(("scheduleUpdate[updateAt]", publishAt))
166
+        fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
167
+        fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
168
+    else:
169
+        fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"])))
170
+
171
+    if options.get('--thumbnail'):
172
+        fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
173
+        fields.append(("previewfile", get_file(options.get('--thumbnail'))))
174
+
175
+    if options.get('--playlist'):
176
+        playlist_id = get_playlist_by_name(user_info, options)
177
+        if not playlist_id and options.get('--playlistCreate'):
178
+            playlist_id = create_playlist(oauth, url, options)
179
+        elif not playlist_id:
180
+            logging.warning("Playlist `" + options.get('--playlist') + "` is unknown, using default playlist.")
181
+            playlist_id = get_default_playlist(user_info)
182
+    else:
183
+        playlist_id = get_default_playlist(user_info)
184
+    fields.append(("channelId", str(playlist_id)))
185
+
108 186
     multipart_data = MultipartEncoder(fields)
109 187
 
110 188
     headers = {
111 189
         'Content-Type': multipart_data.content_type
112 190
     }
113
-
114 191
     response = oauth.post(url + "/api/v1/videos/upload",
115 192
                           data=multipart_data,
116 193
                           headers=headers)
@@ -120,13 +197,11 @@ def upload_video(oauth, secret, options):
120 197
             jresponse = jresponse['video']
121 198
             uuid = jresponse['uuid']
122 199
             idvideo = str(jresponse['id'])
123
-            template = ('Peertube : Video was successfully uploaded.\n'
124
-                        'Watch it at %s/videos/watch/%s.')
200
+            logging.info('Peertube : Video was successfully uploaded.')
201
+            template = 'Peertube: Watch it at %s/videos/watch/%s.'
125 202
             logging.info(template % (url, uuid))
126
-            if options.get('--publishAt'):
127
-                utils.publishAt(str(options.get('--publishAt')), oauth, url, idvideo, secret)
128 203
         else:
129
-            logging.error(('Peertube : The upload failed with an unexpected response: '
204
+            logging.error(('Peertube: The upload failed with an unexpected response: '
130 205
                            '%s') % response)
131 206
             exit(1)
132 207
 
@@ -136,16 +211,16 @@ def run(options):
136 211
     try:
137 212
         secret.read(PEERTUBE_SECRETS_FILE)
138 213
     except Exception as e:
139
-        logging.error("Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
214
+        logging.error("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
140 215
         exit(1)
141 216
     insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT')
142 217
     os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport
143 218
     oauth = get_authenticated_service(secret)
144 219
     try:
145
-        logging.info('Peertube : Uploading file...')
220
+        logging.info('Peertube: Uploading video...')
146 221
         upload_video(oauth, secret, options)
147 222
     except Exception as e:
148 223
         if hasattr(e, 'message'):
149
-            logging.error("Error: " + str(e.message))
224
+            logging.error("Peertube: Error: " + str(e.message))
150 225
         else:
151
-            logging.error("Error: " + str(e))
226
+            logging.error("Peertube: Error: " + str(e))

+ 35
- 66
lib/utils.py View File

@@ -98,6 +98,32 @@ def getLanguage(language, platform):
98 98
         return PEERTUBE_LANGUAGE[language.lower()]
99 99
 
100 100
 
101
+def remove_empty_kwargs(**kwargs):
102
+    good_kwargs = {}
103
+    if kwargs is not None:
104
+        for key, value in kwargs.iteritems():
105
+            if value:
106
+                good_kwargs[key] = value
107
+    return good_kwargs
108
+
109
+def searchThumbnail(options):
110
+    video_directory = dirname(options.get('--file')) + "/"
111
+    # First, check for thumbnail based on videoname
112
+    if options.get('--name'):
113
+        if isfile(video_directory + options.get('--name') + ".jpg"):
114
+            options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
115
+        elif isfile(video_directory + options.get('--name') + ".jpeg"):
116
+            options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
117
+    # Then, if we still not have thumbnail, check for thumbnail based on videofile name
118
+    if not options.get('--thumbnail'):
119
+        video_file = splitext(basename(options.get('--file')))[0]
120
+        if isfile(video_directory + video_file + ".jpg"):
121
+            options['--thumbnail'] = video_directory + video_file + ".jpg"
122
+        elif isfile(video_directory + video_file + ".jpeg"):
123
+            options['--thumbnail'] = video_directory + video_file + ".jpeg"
124
+    return options
125
+
126
+
101 127
 # return the nfo as a RawConfigParser object
102 128
 def loadNFO(options):
103 129
     video_directory = dirname(options.get('--file')) + "/"
@@ -117,7 +143,6 @@ def loadNFO(options):
117 143
     else:
118 144
         if options.get('--name'):
119 145
             nfo_file = video_directory + options.get('--name') + ".txt"
120
-            print nfo_file
121 146
             if isfile(nfo_file):
122 147
                 try:
123 148
                     logging.info("Using " + nfo_file + " as NFO, loading...")
@@ -168,71 +193,15 @@ def parseNFO(options):
168 193
 def upcaseFirstLetter(s):
169 194
     return s[0].upper() + s[1:]
170 195
 
171
-
172
-def publishAt(publishAt, oauth, url, idvideo, secret):
173
-    try:
174
-        FNULL = open(devnull, 'w')
175
-        check_call(["at", "-V"], stdout=FNULL, stderr=STDOUT)
176
-    except CalledProcessError:
177
-        logging.error("You need to install the atd daemon to use the publishAt option.")
178
-        exit(1)
179
-    try:
180
-        FNULL = open(devnull, 'w')
181
-        check_call(["curl", "-V"], stdout=FNULL, stderr=STDOUT)
182
-    except CalledProcessError:
183
-        logging.error("You need to install the curl command line to use the publishAt option.")
184
-        exit(1)
185
-    try:
186
-        FNULL = open(devnull, 'w')
187
-        check_call(["jq", "-V"], stdout=FNULL, stderr=STDOUT)
188
-    except CalledProcessError:
189
-        logging.error("You need to install the jq command line to use the publishAt option.")
190
-        exit(1)
191
-    time = publishAt.split("T")
192
-    # Remove leading seconds that atd does not manage
193
-    if time[1].count(":") == 2:
194
-        time[1] = time[1][:-3]
195
-
196
-    atTime = time[1] + " " + time[0]
197
-    refresh_token=str(oauth.__dict__['_client'].__dict__['refresh_token'])
198
-    atFile = "/tmp/peertube_" + idvideo + "_" + publishAt + ".at"
199
-    try:
200
-        openfile = open(atFile,"w")
201
-        openfile.write('token=$(curl -X POST -d "client_id=' + str(secret.get('peertube', 'client_id')) +
202
-                        '&client_secret=' + str(secret.get('peertube', 'client_secret')) +
203
-                        '&grant_type=refresh_token&refresh_token=' + str(refresh_token) +
204
-                        '" "' + url + '/api/v1/users/token" | jq -r .access_token)')
205
-        openfile.write("\n")
206
-        openfile.write('curl "' + url + '/api/v1/videos/' + idvideo +
207
-                        '" -X PUT -H "Authorization: Bearer ${token}"' +
208
-                        ' -H "Content-Type: multipart/form-data" -F "privacy=1"')
209
-        openfile.write("\n ")  # atd needs an empty line at the end of the file to load...
210
-        openfile.close()
211
-    except Exception as e:
212
-        if hasattr(e, 'message'):
213
-            logging.error("Error: " + str(e.message))
214
-        else:
215
-            logging.error("Error: " + str(e))
216
-
217
-    try:
218
-        FNULL = open(devnull, 'w')
219
-        check_call(["at", "-M", "-f", atFile, atTime], stdout=FNULL, stderr=STDOUT)
220
-    except Exception as e:
221
-        if hasattr(e, 'message'):
222
-            logging.error("Error: " + str(e.message))
223
-        else:
224
-            logging.error("Error: " + str(e))
225
-
226
-
227
-def mastodonTag(tag):
228
-    tags = tag.split(' ')
229
-    mtag = ''
230
-    for s in tags:
196
+def cleanString(toclean):
197
+    toclean = toclean.split(' ')
198
+    cleaned = ''
199
+    for s in toclean:
231 200
         if s == '':
232 201
             continue
233
-        strtag = unicodedata.normalize('NFKD', unicode (s, 'utf-8')).encode('ASCII', 'ignore')
234
-        strtag = ''.join(e for e in strtag if e.isalnum())
235
-        strtag = upcaseFirstLetter(strtag)
236
-        mtag = mtag + strtag
202
+        strtoclean = unicodedata.normalize('NFKD', unicode (s, 'utf-8')).encode('ASCII', 'ignore')
203
+        strtoclean = ''.join(e for e in strtoclean if e.isalnum())
204
+        strtoclean = upcaseFirstLetter(strtoclean)
205
+        cleaned = cleaned + strtoclean
237 206
 
238
-    return mtag
207
+    return cleaned

+ 141
- 10
lib/yt_upload.py View File

@@ -1,4 +1,4 @@
1
-#!/usr/bin/python
1
+#!/usr/bin/env python2
2 2
 # coding: utf-8
3 3
 # From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py  # noqa
4 4
 
@@ -9,6 +9,7 @@ import time
9 9
 import copy
10 10
 import json
11 11
 from os.path import splitext, basename, exists
12
+import os
12 13
 import google.oauth2.credentials
13 14
 import datetime
14 15
 import pytz
@@ -20,6 +21,7 @@ from googleapiclient.errors import HttpError
20 21
 from googleapiclient.http import MediaFileUpload
21 22
 from google_auth_oauthlib.flow import InstalledAppFlow
22 23
 
24
+
23 25
 import utils
24 26
 
25 27
 logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
@@ -50,13 +52,14 @@ RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
50 52
 
51 53
 CLIENT_SECRETS_FILE = 'youtube_secret.json'
52 54
 CREDENTIALS_PATH = ".youtube_credentials.json"
53
-SCOPES = ['https://www.googleapis.com/auth/youtube.upload']
55
+SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
54 56
 API_SERVICE_NAME = 'youtube'
55 57
 API_VERSION = 'v3'
56 58
 
57 59
 
58 60
 # Authorize the request and store authorization credentials.
59 61
 def get_authenticated_service():
62
+    check_authenticated_scopes()
60 63
     flow = InstalledAppFlow.from_client_secrets_file(
61 64
         CLIENT_SECRETS_FILE, SCOPES)
62 65
     if exists(CREDENTIALS_PATH):
@@ -70,7 +73,7 @@ def get_authenticated_service():
70 73
                 client_secret=credential_params["_client_secret"]
71 74
             )
72 75
     else:
73
-        credentials = flow.run_local_server()
76
+        credentials = flow.run_console()
74 77
         with open(CREDENTIALS_PATH, 'w') as f:
75 78
             p = copy.deepcopy(vars(credentials))
76 79
             del p["expiry"]
@@ -78,6 +81,16 @@ def get_authenticated_service():
78 81
     return build(API_SERVICE_NAME, API_VERSION, credentials=credentials,  cache_discovery=False)
79 82
 
80 83
 
84
+def check_authenticated_scopes():
85
+    if exists(CREDENTIALS_PATH):
86
+        with open(CREDENTIALS_PATH, 'r') as f:
87
+            credential_params = json.load(f)
88
+            # Check if all scopes are present
89
+            if credential_params["_scopes"] != SCOPES:
90
+                logging.warning("Youtube: Credentials are obsolete, need to re-authenticate.")
91
+                os.remove(CREDENTIALS_PATH)
92
+
93
+
81 94
 def initialize_upload(youtube, options):
82 95
     path = options.get('--file')
83 96
     tags = None
@@ -120,31 +133,149 @@ def initialize_upload(youtube, options):
120 133
         publishAt = tz.localize(publishAt).isoformat()
121 134
         body['status']['publishAt'] = str(publishAt)
122 135
 
136
+    if options.get('--playlist'):
137
+        playlist_id = get_playlist_by_name(youtube, options.get('--playlist'))
138
+        if not playlist_id and options.get('--playlistCreate'):
139
+            playlist_id = create_playlist(youtube, options.get('--playlist'))
140
+        elif not playlist_id:
141
+            logging.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.")
142
+            logging.warning("If you want to create it, set the --playlistCreate option.")
143
+            playlist_id = ""
144
+    else:
145
+        playlist_id = ""
146
+
123 147
     # Call the API's videos.insert method to create and upload the video.
124 148
     insert_request = youtube.videos().insert(
125 149
         part=','.join(body.keys()),
126 150
         body=body,
127 151
         media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
128 152
     )
129
-    resumable_upload(insert_request)
153
+    video_id = resumable_upload(insert_request, 'video', 'insert')
154
+
155
+    # If we get a video_id, upload is successful and we are able to set thumbnail
156
+    if video_id and options.get('--thumbnail'):
157
+        set_thumbnail(youtube, options.get('--thumbnail'), videoId=video_id)
158
+
159
+    # If we get a video_id, upload is successful and we are able to set playlist
160
+    if video_id and options.get('--playlist'):
161
+        set_playlist(youtube, playlist_id, video_id)
162
+
163
+
164
+def get_playlist_by_name(youtube, playlist_name):
165
+    response = youtube.playlists().list(
166
+        part='snippet,id',
167
+        mine=True,
168
+        maxResults=50
169
+    ).execute()
170
+    for playlist in response["items"]:
171
+        if playlist["snippet"]['title'] == playlist_name:
172
+            return playlist['id']
173
+
174
+
175
+def create_playlist(youtube, playlist_name):
176
+    template = ('Youtube: Playlist %s does not exist, creating it.')
177
+    logging.info(template % (str(playlist_name)))
178
+    resources = build_resource({'snippet.title': playlist_name,
179
+                                'snippet.description': '',
180
+                                'status.privacyStatus': 'public'})
181
+    response = youtube.playlists().insert(
182
+        body=resources,
183
+        part='status,snippet,id'
184
+    ).execute()
185
+    return response["id"]
186
+
187
+
188
+def build_resource(properties):
189
+    resource = {}
190
+    for p in properties:
191
+        # Given a key like "snippet.title", split into "snippet" and "title", where
192
+        # "snippet" will be an object and "title" will be a property in that object.
193
+        prop_array = p.split('.')
194
+        ref = resource
195
+        for pa in range(0, len(prop_array)):
196
+            is_array = False
197
+            key = prop_array[pa]
198
+
199
+            # For properties that have array values, convert a name like
200
+            # "snippet.tags[]" to snippet.tags, and set a flag to handle
201
+            # the value as an array.
202
+            if key[-2:] == '[]':
203
+                key = key[0:len(key)-2:]
204
+                is_array = True
205
+
206
+            if pa == (len(prop_array) - 1):
207
+                # Leave properties without values out of inserted resource.
208
+                if properties[p]:
209
+                    if is_array:
210
+                        ref[key] = properties[p].split(',')
211
+                    else:
212
+                        ref[key] = properties[p]
213
+            elif key not in ref:
214
+                # For example, the property is "snippet.title", but the resource does
215
+                # not yet have a "snippet" object. Create the snippet object here.
216
+                # Setting "ref = ref[key]" means that in the next time through the
217
+                # "for pa in range ..." loop, we will be setting a property in the
218
+                # resource's "snippet" object.
219
+                ref[key] = {}
220
+                ref = ref[key]
221
+            else:
222
+                # For example, the property is "snippet.description", and the resource
223
+                # already has a "snippet" object.
224
+                ref = ref[key]
225
+    return resource
226
+
227
+
228
+def set_thumbnail(youtube, media_file, **kwargs):
229
+    kwargs = utils.remove_empty_kwargs(**kwargs)
230
+    request = youtube.thumbnails().set(
231
+        media_body=MediaFileUpload(media_file, chunksize=-1,
232
+                                   resumable=True),
233
+        **kwargs
234
+    )
235
+
236
+    # See full sample for function
237
+    return resumable_upload(request, 'thumbnail', 'set')
238
+
239
+
240
+def set_playlist(youtube, playlist_id, video_id):
241
+    logging.info('Youtube: Configuring playlist...')
242
+    resource = build_resource({'snippet.playlistId': playlist_id,
243
+                               'snippet.resourceId.kind': 'youtube#video',
244
+                               'snippet.resourceId.videoId': video_id,
245
+                               'snippet.position': ''}
246
+                              )
247
+    try:
248
+        youtube.playlistItems().insert(
249
+            body=resource,
250
+            part='snippet'
251
+        ).execute()
252
+    except Exception as e:
253
+        if hasattr(e, 'message'):
254
+            logging.error("Youtube: Error: " + str(e.message))
255
+        else:
256
+            logging.error("Youtube: Error: " + str(e))
257
+    logging.info('Youtube: Video is correclty added to the playlist.')
130 258
 
131 259
 
132 260
 # This method implements an exponential backoff strategy to resume a
133 261
 # failed upload.
134
-def resumable_upload(request):
262
+def resumable_upload(request, resource, method):
135 263
     response = None
136 264
     error = None
137 265
     retry = 0
138 266
     while response is None:
139 267
         try:
140
-            logging.info('Youtube : Uploading file...')
268
+            template = 'Youtube: Uploading %s...'
269
+            logging.info(template % resource)
141 270
             status, response = request.next_chunk()
142 271
             if response is not None:
143
-                if 'id' in response:
144
-                    template = ('Youtube : Video was successfully '
145
-                                'uploaded.\n'
146
-                                'Watch it at https://youtu.be/%s (post-encoding could take some time)')
272
+                if method == 'insert' and 'id' in response:
273
+                    logging.info('Youtube : Video was successfully uploaded.')
274
+                    template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
147 275
                     logging.info(template % response['id'])
276
+                    return response['id']
277
+                elif method != 'insert' or "id" not in response:
278
+                    logging.info('Youtube: Thumbnail was successfully set.')
148 279
                 else:
149 280
                     template = ('Youtube : The upload failed with an '
150 281
                                 'unexpected response: %s')

+ 3
- 0
nfo_example.txt View File

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

+ 2
- 2
peertube_secret.sample View File

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

+ 25
- 3
prismedia_upload.py View File

@@ -1,4 +1,4 @@
1
-#!/usr/bin/python
1
+#!/usr/bin/env python2
2 2
 # coding: utf-8
3 3
 
4 4
 """
@@ -6,11 +6,12 @@ prismedia_upload - tool to upload videos to Peertube and Youtube
6 6
 
7 7
 Usage:
8 8
   prismedia_upload.py --file=<FILE> [options]
9
-  prismedia_upload.py --file=<FILE> --tags=STRING [--mt options]
9
+  prismedia_upload.py -f <FILE> --tags=STRING [--mt options]
10 10
   prismedia_upload.py -h | --help
11 11
   prismedia_upload.py --version
12 12
 
13 13
 Options:
14
+  -f, --file=STRING Path to the video file to upload in mp4
14 15
   --name=NAME  Name of the video to upload. (default to video filename)
15 16
   -d, --description=STRING  Description of the video. (default: default description)
16 17
   -t, --tags=STRING  Tags for the video. comma separated.
@@ -34,6 +35,13 @@ Options:
34 35
                     DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
35 36
                     DATE should be in the future
36 37
                     For Peertube, requires the "atd" and "curl utilities installed on the system
38
+  --thumbnail=STRING    Path to a file to use as a thumbnail for the video.
39
+                        Supported types are jpg and jpeg.
40
+                        By default, prismedia search for an image based on video name followed by .jpg or .jpeg
41
+  --playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
42
+                    If the playlist is not found, spawn an error except if --playlist-create is set.
43
+  --playlistCreate  Create the playlist if not exists. (default do not create)
44
+                    Only relevant if --playlist is set.
37 45
   -h --help  Show this help.
38 46
   --version  Show version.
39 47
 
@@ -86,7 +94,7 @@ except ImportError:
86 94
                   'see https://github.com/ahupp/python-magic\n')
87 95
     exit(1)
88 96
 
89
-VERSION = "prismedia v0.5"
97
+VERSION = "prismedia v0.6"
90 98
 
91 99
 VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
92 100
 VALID_CATEGORIES = (
@@ -151,6 +159,12 @@ def validatePublish(publish):
151 159
         return False
152 160
     return True
153 161
 
162
+def validateThumbnail(thumbnail):
163
+    supported_types = ['image/jpg', 'image/jpeg']
164
+    if magic.from_file(thumbnail, mime=True) in supported_types:
165
+        return thumbnail
166
+    else:
167
+        return False
154 168
 
155 169
 if __name__ == '__main__':
156 170
 
@@ -199,12 +213,20 @@ if __name__ == '__main__':
199 213
         Optional('--cca'): bool,
200 214
         Optional('--disable-comments'): bool,
201 215
         Optional('--nsfw'): bool,
216
+        Optional('--thumbnail'): Or(None, And(
217
+                                    str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'),
218
+                                    ),
219
+        Optional('--playlist'): Or(None, str),
220
+        Optional('--playlistCreate'): bool,
202 221
         '--help': bool,
203 222
         '--version': bool
204 223
     })
205 224
 
206 225
     options = utils.parseNFO(options)
207 226
 
227
+    if not options.get('--thumbnail'):
228
+        options = utils.searchThumbnail(options)
229
+
208 230
     try:
209 231
         options = schema.validate(options)
210 232
     except SchemaError as e:

Loading…
Cancel
Save