tarina

git clone https://git.tarina.org/tarina
Log | Files | Refs | README | LICENSE

commit 77d2eb313466a20d9b51732a34d5b708456f2a84
parent f1154bd409db1be4ab298cc31961a955f730e0f7
Author: rob <rob@tarina.org>
Date:   Tue, 23 Apr 2019 07:53:37 +0100

youtube upload mod

Diffstat:
Minstall.sh | 13+++++++++++++
Amods/install-youtube-upload.sh | 20++++++++++++++++++++
Amods/upload-mods-enabled | 1+
Amods/youtube-upload.sh | 3+++
Amods/youtube-upload/.github/ISSUE_TEMPLATE/bug_report.md | 23+++++++++++++++++++++++
Amods/youtube-upload/.gitignore | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/Dockerfile | 14++++++++++++++
Amods/youtube-upload/README.md | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/bin/youtube-upload | 10++++++++++
Amods/youtube-upload/bin/youtube-upload.bat | 1+
Amods/youtube-upload/examples/split_video_for_youtube.sh | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/setup.py | 37+++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/__init__.py | 1+
Amods/youtube-upload/youtube_upload/__main__.py | 19+++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/__init__.py | 42++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/browser.py | 19+++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/console.py | 14++++++++++++++
Amods/youtube-upload/youtube_upload/auth/webkit_gtk.py | 48++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/auth/webkit_qt.py | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/categories.py | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/lib.py | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/main.py | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/playlists.py | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/youtube-upload/youtube_upload/upload_video.py | 43+++++++++++++++++++++++++++++++++++++++++++
Mtarina.py | 47+++++++++++++++++++++++++++++++++++------------
25 files changed, 1146 insertions(+), 12 deletions(-)

diff --git a/install.sh b/install.sh @@ -171,6 +171,19 @@ cp extras/.vimrc /home/pi/.vimrc esac done +while true; do + read -p "Do you wish to install the youtube upload mod [y]es or [n]o?" yn + case $yn in + [Yy]* ) echo "Install youtube upload mod..." +cd mods +./install-youtube-upload.sh +cd .. + break;; + [Nn]* ) echo "Nope, good, we dont want googly spyware everywhere! if you however wish to install it later go to the mods folder and run install-youtube-uploader.";break;; + * ) echo "Please answer yes or no.";; + esac +done + echo "Setting up network configuration to use wicd program..." echo "it works nicer from the terminal than raspberry pi default" apt-get -y purge dhcpcd5 plymouth diff --git a/mods/install-youtube-upload.sh b/mods/install-youtube-upload.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Install script for youtube upload mod +# https://github.com/tokland/youtube-upload + +ROOT_UID=0 # Root has $UID 0. + +if [ "$UID" -eq "$ROOT_UID" ] +then + echo "OK" +else + echo "Run with sudo!" + exit 0 +fi + +echo "INSTALLING AND ENABLING MOD: youtube-upload" + +sudo pip3 install --upgrade google-api-python-client oauth2client progressbar2 + +echo "youtube-upload" >> mods-enabled diff --git a/mods/upload-mods-enabled b/mods/upload-mods-enabled @@ -0,0 +1 @@ +youtube-upload title filename diff --git a/mods/youtube-upload.sh b/mods/youtube-upload.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +python3 youtube-upload/youtube_upload/__main__.py $1 $2 diff --git a/mods/youtube-upload/.github/ISSUE_TEMPLATE/bug_report.md b/mods/youtube-upload/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +I am not working on youtube-upload anymore. However, if you prepare a PR, I'll gladly merge it. Please write me (@tokland) if you want to become the maintainer of youtube-upload. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Desktop (please complete the following information):** + - OS: [e.g. GNU/Linux] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/mods/youtube-upload/.gitignore b/mods/youtube-upload/.gitignore @@ -0,0 +1,59 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/+ \ No newline at end of file diff --git a/mods/youtube-upload/Dockerfile b/mods/youtube-upload/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.7-alpine3.8 + +ENV workdir /data +WORKDIR ${workdir} + +RUN mkdir -p ${workdir} && adduser python --disabled-password +COPY . ${workdir} +WORKDIR ${workdir} +RUN pip install --upgrade google-api-python-client oauth2client progressbar2 && \ + python setup.py install + +USER python + +ENTRYPOINT ["youtube-upload"] diff --git a/mods/youtube-upload/README.md b/mods/youtube-upload/README.md @@ -0,0 +1,161 @@ +**THIS PROJECT NEEDS A MAINTAINER**. If someone is willing to take over the project, let me know (pyarnau AT gmail.com). + +Introduction +============ + +Command-line script to upload videos to Youtube using theYoutube [APIv3](https://developers.google.com/youtube/v3/). It should work on any platform (GNU/Linux, BSD, OS X, Windows, ...) that runs Python. + +Dependencies +============ + + * [Python 2.6/2.7/3.x](http://www.python.org). + * Packages: [google-api-python-client](https://developers.google.com/api-client-library/python), [progressbar2](https://pypi.python.org/pypi/progressbar2) (optional). + +Check if your operating system provides those packages (check also those [deb/rpm/mac files](https://github.com/qiuwei/youtube-upload/releases)), otherwise install them with `pip`: + +``` +$ sudo pip install --upgrade google-api-python-client oauth2client progressbar2 +``` + +Install +======= + +``` +$ wget https://github.com/tokland/youtube-upload/archive/master.zip +$ unzip master.zip +$ cd youtube-upload-master +$ sudo python setup.py install +``` + +Or run directly from sources: + +``` +$ cd youtube-upload-master +$ PYTHONPATH=. python bin/youtube-upload ... +``` + +Setup +===== + +You'll see that there is no email/password options. Instead, the Youtube API uses [OAuth 2.0](https://developers.google.com/accounts/docs/OAuth2) to authenticate the upload. The first time you try to upload a video, you will be asked to follow a URL in your browser to get an authentication token. If you have multiple channels for the logged in user, you will also be asked to pick which one you want to upload the videos to. You can use multiple credentials, just use the option ```--credentials-file```. Also, check the [token expiration](https://developers.google.com/youtube/v3/) policies. + +The package used to include a default ```client_secrets.json``` file. It does not work anymore, Google has revoked it. So you now must [create and use your own OAuth 2.0 file](https://developers.google.com/youtube/registering_an_application), it's a free service. Steps: + +* Go to the Google [console](https://console.developers.google.com/). +* _Create project_. +* Side menu: _APIs & auth_ -> _APIs_ +* Top menu: _Enabled API(s)_: Enable all Youtube APIs. +* Side menu: _APIs & auth_ -> _Credentials_. +* _Create a Client ID_: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: youtube-upload -> Create -> OK +* _Download JSON_: Under the section "OAuth 2.0 client IDs". Save the file to your local system. +* Use this JSON as your credentials file: `--client-secrets=CLIENT_SECRETS` or copy it to `~/client_secrets.json`. + +*Note: ```client_secrets.json``` is a file you can download from the developer console, the credentials file is something auto generated after the first time the script is run and the google account sign in is followed, the file is stored at ```~/.youtube-upload-credentials.json```.* + +Examples +======== + +* Upload a video (a valid `~/.client_secrets.json` should exist, check the Setup section): + +``` +$ youtube-upload --title="A.S. Mutter" anne_sophie_mutter.flv +pxzZ-fYjeYs +``` + +* Upload a video with extra metadata, with your own client secrets and credentials file, and to a playlist (if not found, it will be created): + +``` +$ youtube-upload \ + --title="A.S. Mutter" " \ + --description="A.S. Mutter plays Beethoven" \ + --category="Music" \ + --tags="mutter, beethoven" \ + --recording-date="2011-03-10T15:32:17.0Z" \ + --default-language="en" \ + --default-audio-language="en" \ + --client-secrets="my_client_secrets.json" \ + --credentials-file="my_credentials.json" \ + --playlist="My favorite music" \ + --embeddable=True|False \ + anne_sophie_mutter.flv +tx2Zb-145Yz +``` +*Other extra medata available :* + ``` + --privacy (public | unlisted | private) + --publish-at (YYYY-MM-DDThh:mm:ss.sZ) + --location (latitude=VAL,longitude=VAL[,altitude=VAL]) + --thumbnail (string) + ``` + +* Upload a video using a browser GUI to authenticate: + +``` +$ youtube-upload --title="A.S. Mutter" --auth-browser anne_sophie_mutter.flv +``` + +* Split a video with _ffmpeg_ + +If your video is too big or too long for Youtube limits, split it before uploading: + +``` +$ bash examples/split_video_for_youtube.sh video.avi +video.part1.avi +video.part2.avi +video.part3.avi +``` +* Use a HTTP proxy + +Set environment variables *http_proxy* and *https_proxy*: + +``` +$ export http_proxy=http://user:password@host:port +$ export https_proxy=$http_proxy +$ youtube-upload .... +``` + +Get available categories +======================== + +* Go to the [API Explorer](https://developers.google.com/apis-explorer) +- Search "youtube categories" -> *youtube.videoCategories.list* +- This bring you to [youtube.videoCategories.list service](https://developers.google.com/apis-explorer/#search/youtube%20categories/m/youtube/v3/youtube.videoCategories.list) +- part: `id,snippet` +- regionCode: `es` (2 letter code of your country) +- _Authorize and execute_ + +And see the JSON response below. Note that categories with the attribute `assignable` equal to `false` cannot be used. + +Using [shoogle](https://github.com/tokland/shoogle): + +``` +$ shoogle execute --client-secret-file client_secret.json \ + youtube:v3.videoCategories.list <(echo '{"part": "id,snippet", "regionCode": "es"}') | + jq ".items[] | select(.snippet.assignable) | {id: .id, title: .snippet.title}" +``` + +Notes for developers +==================== + +* Main logic of the upload: [main.py](youtube_upload/main.py) (function ```upload_video```). +* Check the [Youtube Data API](https://developers.google.com/youtube/v3/docs/). +* Some Youtube API [examples](https://github.com/youtube/api-samples/tree/master/python) provided by Google. + +Alternatives +============ + +* [shoogle](https://github.com/tokland/shoogle) can send requests to any Google API service, so it can be used not only to upload videos, but also to perform any operation regarding the Youtube API. + +* [youtubeuploader](https://github.com/porjo/youtubeuploader) uploads videos to Youtube from local disk or from the web. It also provides rate-limited uploads. + +More +==== + +* License: [GNU/GPLv3](http://www.gnu.org/licenses/gpl.html). + +Feedback +======== + +* [Donations](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=pyarnau%40gmail%2ecom&lc=US&item_name=youtube%2dupload&no_note=0&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHostedGuest). +* If you find a bug, [open an issue](https://github.com/tokland/youtube-upload/issues). +* If you want a new feature to be added, you'll have to send a pull request (or find a programmer to do it for you), currently I am not adding new features. diff --git a/mods/youtube-upload/bin/youtube-upload b/mods/youtube-upload/bin/youtube-upload @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +if __name__ == '__main__': + + #Allows you to a relative import from the parent folder + import os.path, sys + sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) + + from youtube_upload import main + main.run() diff --git a/mods/youtube-upload/bin/youtube-upload.bat b/mods/youtube-upload/bin/youtube-upload.bat @@ -0,0 +1 @@ +python %~dp0youtube-upload %* diff --git a/mods/youtube-upload/examples/split_video_for_youtube.sh b/mods/youtube-upload/examples/split_video_for_youtube.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Split a video file (to MKV format) suitable for standard users in Youtube (<15') +# +# $ bash split_video_for_youtube.sh video.avi +# video.part1.mkv +# video.part2.mkv +# +# $ youtube-upload [OPTIONS] video.part*.mkv +# + +# Echo to standard error +debug() { + echo "$@" >&2 +} + +# Returns duration (in seconds) of a video $1 (uses ffmpeg). +get_video_duration() { + OUTPUT=$(ffmpeg -i "$1" -vframes 1 -f rawvideo -y /dev/null 2>&1) || + { debug -e "get_video_duration: error running ffmpeg:\n$OUTPUT"; return 1; } + DURATION=$(echo "$OUTPUT" | grep -m1 "^[[:space:]]*Duration:" | + cut -d":" -f2- | cut -d"," -f1 | sed "s/[:\.]/ /g") || + { debug -e "get_video_duration: error parsing duration:\n$OUTPUT"; return 1; } + read HOURS MINUTES SECONDS DECISECONDS <<< "$DURATION" + echo $((10#$HOURS * 3600 + 10#$MINUTES * 60 + 10#$SECONDS)) +} + +main() { + set -e -u -o pipefail + if test $# -eq 0; then + debug "Usage: $(basename $0) VIDEO [EXTRA_OPTIONS_FOR_FFMPEG]" + exit 1 + fi + CHUNK_DURATION=$((60*15)) + VIDEO=$1 + shift 1 + + DURATION=$(get_video_duration "$VIDEO") + if test $DURATION -le $CHUNK_DURATION; then + debug "no need to split, duration of video: $DURATION <= $CHUNK_DURATION" + echo "$VIDEO" + exit 0 + fi + + EXTENSION=${VIDEO##*.} + BASENAME=$(basename "$VIDEO" ".$EXTENSION") + debug "start split: $VIDEO ($DURATION seconds)" + seq 0 $CHUNK_DURATION $DURATION | cat -n | while read INDEX OFFSET; do + debug "$VIDEO: from position $OFFSET take $CHUNK_DURATION seconds" + PADDED_INDEX=$(printf '%03d' $INDEX) + OUTPUT_FILE="${BASENAME}.part${PADDED_INDEX}.mkv" + ffmpeg -i "$VIDEO" -vcodec copy -acodec copy "$@" \ + -ss $OFFSET -t $CHUNK_DURATION -y "$OUTPUT_FILE" </dev/null + echo "$OUTPUT_FILE" + done +} + +test "$NOEXEC" = 1 || main "$@" diff --git a/mods/youtube-upload/setup.py b/mods/youtube-upload/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +"""Upload videos to Youtube.""" +from distutils.core import setup + +setup_kwargs = { + "name": "youtube-upload", + "version": "0.8.0", + "description": "Upload videos to Youtube", + "author": "Arnau Sanchez", + "author_email": "pyarnau@gmail.com", + "url": "https://github.com/tokland/youtube-upload", + "packages": ["youtube_upload/", "youtube_upload/auth"], + "scripts": ["bin/youtube-upload"], + "license": "GNU Public License v3.0", + "long_description": " ".join(__doc__.strip().splitlines()), + "classifiers": [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + ], + "entry_points": { + 'console_scripts': [ + 'youtube-upload = youtube_upload.main:run' + ], + }, + "install_requires":[ + 'google-api-python-client', + 'progressbar2' + ] +} + +setup(**setup_kwargs) diff --git a/mods/youtube-upload/youtube_upload/__init__.py b/mods/youtube-upload/youtube_upload/__init__.py @@ -0,0 +1 @@ +VERSION = "0.8.0" diff --git a/mods/youtube-upload/youtube_upload/__main__.py b/mods/youtube-upload/youtube_upload/__main__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +from __future__ import unicode_literals + +# Execute with +# $ python youtube_upload/__main__.py (2.6+) +# $ python -m youtube_upload (2.7+) + +import sys + +if __package__ is None and not hasattr(sys, 'frozen'): + # direct call of __main__.py + import os.path + path = os.path.realpath(os.path.abspath(__file__)) + sys.path.insert(0, os.path.dirname(os.path.dirname(path))) + +import youtube_upload.main + +if __name__ == '__main__': + youtube_upload.main.main(sys.argv[1:]) diff --git a/mods/youtube-upload/youtube_upload/auth/__init__.py b/mods/youtube-upload/youtube_upload/auth/__init__.py @@ -0,0 +1,42 @@ +"""Wrapper for Google OAuth2 API.""" +import sys +import json + +import googleapiclient.discovery +import oauth2client +import httplib2 + +from youtube_upload import lib +from youtube_upload.auth import console +from youtube_upload.auth import browser + +YOUTUBE_UPLOAD_SCOPE = ["https://www.googleapis.com/auth/youtube.upload", "https://www.googleapis.com/auth/youtube"] + +def _get_credentials_interactively(flow, storage, get_code_callback): + """Return the credentials asking the user.""" + flow.redirect_uri = oauth2client.client.OOB_CALLBACK_URN + authorize_url = flow.step1_get_authorize_url() + code = get_code_callback(authorize_url) + if code: + credential = flow.step2_exchange(code, http=None) + storage.put(credential) + credential.set_store(storage) + return credential + +def _get_credentials(flow, storage, get_code_callback): + """Return the user credentials. If not found, run the interactive flow.""" + existing_credentials = storage.get() + if existing_credentials and not existing_credentials.invalid: + return existing_credentials + else: + return _get_credentials_interactively(flow, storage, get_code_callback) + +def get_resource(client_secrets_file, credentials_file, get_code_callback): + """Authenticate and return a googleapiclient.discovery.Resource object.""" + get_flow = oauth2client.client.flow_from_clientsecrets + flow = get_flow(client_secrets_file, scope=YOUTUBE_UPLOAD_SCOPE) + storage = oauth2client.file.Storage(credentials_file) + credentials = _get_credentials(flow, storage, get_code_callback) + if credentials: + http = credentials.authorize(httplib2.Http()) + return googleapiclient.discovery.build("youtube", "v3", http=http) diff --git a/mods/youtube-upload/youtube_upload/auth/browser.py b/mods/youtube-upload/youtube_upload/auth/browser.py @@ -0,0 +1,19 @@ +from .. import lib + +try: + from youtube_upload.auth import webkit_qt as backend + WEBKIT_BACKEND = "qt" +except ImportError: + try: + from youtube_upload.auth import webkit_gtk as backend + WEBKIT_BACKEND = "gtk" + except ImportError: + WEBKIT_BACKEND = None + +def get_code(url, size=(640, 480), title="Google authentication"): + if WEBKIT_BACKEND: + lib.debug("Using webkit backend: " + WEBKIT_BACKEND) + with lib.default_sigint(): + return backend.get_code(url, size=size, title=title) + else: + raise NotImplementedError("GUI auth requires pywebkitgtk or qtwebkit") diff --git a/mods/youtube-upload/youtube_upload/auth/console.py b/mods/youtube-upload/youtube_upload/auth/console.py @@ -0,0 +1,13 @@ +import sys + +def get_code(authorize_url): + + """Show authorization URL and return the code the user wrote.""" + message = "Check this link in your browser: {0}".format(authorize_url) + sys.stderr.write(message + "\n") + try: input = raw_input #For Python2 compatability + except NameError: + #For Python3 on Windows compatability + try: from builtins import input as input + except ImportError: pass + return input("Enter verification code: ")+ \ No newline at end of file diff --git a/mods/youtube-upload/youtube_upload/auth/webkit_gtk.py b/mods/youtube-upload/youtube_upload/auth/webkit_gtk.py @@ -0,0 +1,48 @@ +import json + +CHECK_AUTH_JS = """ + var code = document.getElementById("code"); + var access_denied = document.getElementById("access_denied"); + var result; + + if (code) { + result = {authorized: true, code: code.value}; + } else if (access_denied) { + result = {authorized: false, message: access_denied.innerText}; + } else { + result = {}; + } + window.status = JSON.stringify(result); +""" + +def _on_webview_status_bar_changed(webview, status, dialog): + if status: + authorization = json.loads(status) + if authorization.has_key("authorized"): + dialog.set_data("authorization_code", authorization["code"]) + dialog.response(0) + +def get_code(url, size=(640, 480), title="Google authentication"): + """Open a GTK webkit window and return the access code.""" + import gtk + import webkit + dialog = gtk.Dialog(title=title) + webview = webkit.WebView() + scrolled = gtk.ScrolledWindow() + scrolled.add(webview) + dialog.get_children()[0].add(scrolled) + webview.load_uri(url) + dialog.resize(*size) + dialog.show_all() + dialog.connect("delete-event", + lambda event, data: dialog.response(1)) + webview.connect("load-finished", + lambda view, frame: view.execute_script(CHECK_AUTH_JS)) + webview.connect("status-bar-text-changed", + _on_webview_status_bar_changed, dialog) + dialog.set_data("authorization_code", None) + status = dialog.run() + dialog.destroy() + while gtk.events_pending(): + gtk.main_iteration(False) + return dialog.get_data("authorization_code") diff --git a/mods/youtube-upload/youtube_upload/auth/webkit_qt.py b/mods/youtube-upload/youtube_upload/auth/webkit_qt.py @@ -0,0 +1,54 @@ +CHECK_AUTH_JS = """ + var code = document.getElementById("code"); + var access_denied = document.getElementById("access_denied"); + var result; + + if (code) { + result = {authorized: true, code: code.value}; + } else if (access_denied) { + result = {authorized: false, message: access_denied.innerText}; + } else { + result = {}; + } + result; +""" + +def _on_qt_page_load_finished(dialog, webview): + to_s = lambda x: (str(x.toUtf8()) if hasattr(x,'toUtf8') else x) + frame = webview.page().currentFrame() + try: #PySide does not QStrings + from QtCore import QString + jscode = QString(CHECK_AUTH_JS) + except ImportError: + jscode = CHECK_AUTH_JS + res = frame.evaluateJavaScript(jscode) + try: + authorization = dict((to_s(k), to_s(v)) for (k, v) in res.toPyObject().items()) + except AttributeError: #PySide returns the result in pure Python + authorization = dict((to_s(k), to_s(v)) for (k, v) in res.items()) + if "authorized" in authorization: + dialog.authorization_code = authorization.get("code") + dialog.close() + +def get_code(url, size=(640, 480), title="Google authentication"): + """Open a QT webkit window and return the access code.""" + try: + from PyQt4 import QtCore, QtGui, QtWebKit + except ImportError: + from PySide import QtCore, QtGui, QtWebKit + app = QtGui.QApplication([]) + dialog = QtGui.QDialog() + dialog.setWindowTitle(title) + dialog.resize(*size) + webview = QtWebKit.QWebView() + webpage = QtWebKit.QWebPage() + webview.setPage(webpage) + webpage.loadFinished.connect(lambda: _on_qt_page_load_finished(dialog, webview)) + webview.setUrl(QtCore.QUrl.fromEncoded(url)) + layout = QtGui.QGridLayout() + layout.addWidget(webview) + dialog.setLayout(layout) + dialog.authorization_code = None + dialog.show() + app.exec_() + return dialog.authorization_code diff --git a/mods/youtube-upload/youtube_upload/categories.py b/mods/youtube-upload/youtube_upload/categories.py @@ -0,0 +1,51 @@ +try: + #import urllib2 + from urllib2 import urlopen + import urllib +except ImportError: + from urllib.request import urlopen +import json + +URL = "https://www.googleapis.com/youtube/v3/videoCategories" + +IDS = { + "Film & Animation": 1, + "Autos & Vehicles": 2, + "Music": 10, + "Pets & Animals": 15, + "Sports": 17, + "Short Movies": 18, + "Travel & Events": 19, + "Gaming": 20, + "Videoblogging": 21, + "People & Blogs": 22, + "Comedy": 23, + "Entertainment": 24, + "News & Politics": 25, + "Howto & Style": 26, + "Education": 27, + "Science & Technology": 28, + "Nonprofits & Activism": 29, + "Movies": 30, + "Anime/Animation": 31, + "Action/Adventure": 32, + "Classics": 33, + "Documentary": 35, + "Drama": 36, + "Family": 37, + "Foreign": 38, + "Horror": 39, + "Sci-Fi/Fantasy": 40, + "Thriller": 41, + "Shorts": 42, + "Shows": 43, + "Trailers": 44, +} + +def get(region_code="us", api_key=None): + params = dict(part="snippet", regionCode=region_code, key=api_key) + full_url = URL + "?" + urllib.urlencode(params) + response = urlopen(full_url) + categories_info = json.loads(response.read()) + items = categories_info["items"] + return dict((item["snippet"]["title"], item["id"]) for item in items) diff --git a/mods/youtube-upload/youtube_upload/lib.py b/mods/youtube-upload/youtube_upload/lib.py @@ -0,0 +1,94 @@ +from __future__ import print_function +import os +import sys +import locale +import random +import time +import signal +from contextlib import contextmanager + +import googleapiclient.errors + +@contextmanager +def default_sigint(): + original_sigint_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, signal.SIG_DFL) + try: + yield + finally: + signal.signal(signal.SIGINT, original_sigint_handler) + +def get_encoding(): + return locale.getpreferredencoding() + +def to_utf8(s): + """Re-encode string from the default system encoding to UTF-8.""" + current = locale.getpreferredencoding() + if hasattr(s, 'decode'): #Python 3 workaround + return (s.decode(current).encode("UTF-8") if s and current != "UTF-8" else s) + elif isinstance(s, bytes): + return bytes.decode(s) + else: + return s + +def debug(obj, fd=sys.stderr): + """Write obj to standard error.""" + print(obj, file=fd) + +def catch_exceptions(exit_codes, fun, *args, **kwargs): + """ + Catch exceptions on fun(*args, **kwargs) and return the exit code specified + in the exit_codes dictionary. Return 0 if no exception is raised. + """ + try: + fun(*args, **kwargs) + return 0 + except tuple(exit_codes.keys()) as exc: + debug("[{0}] {1}".format(exc.__class__.__name__, exc)) + return exit_codes[exc.__class__] + +def first(it): + """Return first element in iterable.""" + return it.next() + +def string_to_dict(string): + """Return dictionary from string "key1=value1, key2=value2".""" + if string: + pairs = [s.strip() for s in string.split(",")] + return dict(pair.split("=") for pair in pairs) + +def get_first_existing_filename(prefixes, relative_path): + """Get the first existing filename of relative_path seeking on prefixes directories.""" + for prefix in prefixes: + path = os.path.join(prefix, relative_path) + if os.path.exists(path): + return path + +def retriable_exceptions(fun, retriable_exceptions, max_retries=None): + """Run function and retry on some exceptions (with exponential backoff).""" + retry = 0 + while 1: + try: + return fun() + except tuple(retriable_exceptions) as exc: + retry += 1 + if type(exc) not in retriable_exceptions: + raise exc + # we want to retry 5xx errors only + elif type(exc) == googleapiclient.errors.HttpError and exc.resp.status < 500: + raise exc + elif max_retries is not None and retry > max_retries: + debug("[Retryable errors] Retry limit reached") + raise exc + else: + seconds = random.uniform(0, 2**retry) + message = ("[Retryable error {current_retry}/{total_retries}] " + + "{error_type} ({error_msg}). Wait {wait_time} seconds").format( + current_retry=retry, + total_retries=max_retries or "-", + error_type=type(exc).__name__, + error_msg=str(exc) or "-", + wait_time="%.1f" % seconds, + ) + debug(message) + time.sleep(seconds) diff --git a/mods/youtube-upload/youtube_upload/main.py b/mods/youtube-upload/youtube_upload/main.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python +# +# Upload videos to Youtube from the command-line using APIv3. +# +# Author: Arnau Sanchez <pyarnau@gmail.com> +# Project: https://github.com/tokland/youtube-upload +""" +Upload a video to Youtube from the command-line. + + $ youtube-upload --title="A.S. Mutter playing" \ + --description="Anne Sophie Mutter plays Beethoven" \ + --category=Music \ + --tags="mutter, beethoven" \ + anne_sophie_mutter.flv + pxzZ-fYjeYs +""" + +import os +import sys +import optparse +import collections +import webbrowser +from io import open + +import googleapiclient.errors +import oauth2client +from oauth2client import file + +from . import auth +from . import upload_video +from . import categories +from . import lib +from . import playlists + +# http://code.google.com/p/python-progressbar (>= 2.3) +try: + import progressbar +except ImportError: + progressbar = None + +class InvalidCategory(Exception): pass +class OptionsError(Exception): pass +class AuthenticationError(Exception): pass +class RequestError(Exception): pass + +EXIT_CODES = { + OptionsError: 2, + InvalidCategory: 3, + RequestError: 3, + AuthenticationError: 4, + oauth2client.client.FlowExchangeError: 4, + NotImplementedError: 5, +} + +WATCH_VIDEO_URL = "https://www.youtube.com/watch?v={id}" + +debug = lib.debug +struct = collections.namedtuple + +def open_link(url): + """Opens a URL link in the client's browser.""" + webbrowser.open(url) + +def get_progress_info(): + """Return a function callback to update the progressbar.""" + progressinfo = struct("ProgressInfo", ["callback", "finish"]) + + if progressbar: + bar = progressbar.ProgressBar(widgets=[ + progressbar.Percentage(), + ' ', progressbar.Bar(), + ' ', progressbar.FileTransferSpeed(), + ' ', progressbar.DataSize(), '/', progressbar.DataSize('max_value'), + ' ', progressbar.Timer(), + ' ', progressbar.AdaptiveETA(), + ]) + def _callback(total_size, completed): + if not hasattr(bar, "next_update"): + if hasattr(bar, "maxval"): + bar.maxval = total_size + else: + bar.max_value = total_size + bar.start() + bar.update(completed) + def _finish(): + if hasattr(bar, "next_update"): + return bar.finish() + return progressinfo(callback=_callback, finish=_finish) + else: + return progressinfo(callback=None, finish=lambda: True) + +def get_category_id(category): + """Return category ID from its name.""" + if category: + if category in categories.IDS: + ncategory = categories.IDS[category] + debug("Using category: {0} (id={1})".format(category, ncategory)) + return str(categories.IDS[category]) + else: + msg = "{0} is not a valid category".format(category) + raise InvalidCategory(msg) + +def upload_youtube_video(youtube, options, video_path, total_videos, index): + """Upload video with index (for split videos).""" + u = lib.to_utf8 + title = u(options.title) + if hasattr(u('string'), 'decode'): + description = u(options.description or "").decode("string-escape") + else: + description = options.description + if options.publish_at: + debug("Your video will remain private until specified date.") + + tags = [u(s.strip()) for s in (options.tags or "").split(",")] + ns = dict(title=title, n=index+1, total=total_videos) + title_template = u(options.title_template) + complete_title = (title_template.format(**ns) if total_videos > 1 else title) + progress = get_progress_info() + category_id = get_category_id(options.category) + request_body = { + "snippet": { + "title": complete_title, + "description": description, + "categoryId": category_id, + "tags": tags, + "defaultLanguage": options.default_language, + "defaultAudioLanguage": options.default_audio_language, + + }, + "status": { + "embeddable": options.embeddable, + "privacyStatus": ("private" if options.publish_at else options.privacy), + "publishAt": options.publish_at, + "license": options.license, + + }, + "recordingDetails": { + "location": lib.string_to_dict(options.location), + "recordingDate": options.recording_date, + }, + } + + debug("Start upload: {0}".format(video_path)) + try: + video_id = upload_video.upload(youtube, video_path, + request_body, progress_callback=progress.callback, + chunksize=options.chunksize) + finally: + progress.finish() + return video_id + +def get_youtube_handler(options): + """Return the API Youtube object.""" + home = os.path.expanduser("~") + default_credentials = os.path.join(home, ".youtube-upload-credentials.json") + client_secrets = options.client_secrets or os.path.join(home, ".client_secrets.json") + credentials = options.credentials_file or default_credentials + debug("Using client secrets: {0}".format(client_secrets)) + debug("Using credentials file: {0}".format(credentials)) + get_code_callback = (auth.browser.get_code + if options.auth_browser else auth.console.get_code) + return auth.get_resource(client_secrets, credentials, + get_code_callback=get_code_callback) + +def parse_options_error(parser, options): + """Check errors in options.""" + required_options = ["title"] + missing = [opt for opt in required_options if not getattr(options, opt)] + if missing: + parser.print_usage() + msg = "Some required option are missing: {0}".format(", ".join(missing)) + raise OptionsError(msg) + +def run_main(parser, options, args, output=sys.stdout): + """Run the main scripts from the parsed options/args.""" + parse_options_error(parser, options) + youtube = get_youtube_handler(options) + + if youtube: + for index, video_path in enumerate(args): + video_id = upload_youtube_video(youtube, options, video_path, len(args), index) + video_url = WATCH_VIDEO_URL.format(id=video_id) + debug("Video URL: {0}".format(video_url)) + if options.open_link: + open_link(video_url) #Opens the Youtube Video's link in a webbrowser + + if options.thumb: + youtube.thumbnails().set(videoId=video_id, media_body=options.thumb).execute() + if options.playlist: + playlists.add_video_to_playlist(youtube, video_id, + title=lib.to_utf8(options.playlist), privacy=options.privacy) + output.write(video_id + "\n") + else: + raise AuthenticationError("Cannot get youtube resource") + +def main(arguments): + """Upload videos to Youtube.""" + usage = """Usage: %prog [OPTIONS] VIDEO [VIDEO2 ...] + + Upload videos to Youtube.""" + parser = optparse.OptionParser(usage) + + # Video metadata + parser.add_option('-t', '--title', dest='title', type="string", + help='Video title') + parser.add_option('-c', '--category', dest='category', type="string", + help='Video category') + parser.add_option('-d', '--description', dest='description', type="string", + help='Video description') + parser.add_option('', '--description-file', dest='description_file', type="string", + help='Video description file', default=None) + parser.add_option('', '--tags', dest='tags', type="string", + help='Video tags (separated by commas: "tag1, tag2,...")') + parser.add_option('', '--privacy', dest='privacy', metavar="STRING", + default="public", help='Privacy status (public | unlisted | private)') + parser.add_option('', '--publish-at', dest='publish_at', metavar="datetime", + default=None, help='Publish date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ') + parser.add_option('', '--license', dest='license', metavar="string", + choices=('youtube', 'creativeCommon'), default='youtube', + help='License for the video, either "youtube" (the default) or "creativeCommon"') + parser.add_option('', '--location', dest='location', type="string", + default=None, metavar="latitude=VAL,longitude=VAL[,altitude=VAL]", + help='Video location"') + parser.add_option('', '--recording-date', dest='recording_date', metavar="datetime", + default=None, help="Recording date (ISO 8601): YYYY-MM-DDThh:mm:ss.sZ") + parser.add_option('', '--default-language', dest='default_language', type="string", + default=None, metavar="string", + help="Default language (ISO 639-1: en | fr | de | ...)") + parser.add_option('', '--default-audio-language', dest='default_audio_language', type="string", + default=None, metavar="string", + help="Default audio language (ISO 639-1: en | fr | de | ...)") + parser.add_option('', '--thumbnail', dest='thumb', type="string", metavar="FILE", + help='Image file to use as video thumbnail (JPEG or PNG)') + parser.add_option('', '--playlist', dest='playlist', type="string", + help='Playlist title (if it does not exist, it will be created)') + parser.add_option('', '--title-template', dest='title_template', + type="string", default="{title} [{n}/{total}]", metavar="string", + help='Template for multiple videos (default: {title} [{n}/{total}])') + parser.add_option('', '--embeddable', dest='embeddable', default=True, + help='Video is embeddable') + + # Authentication + parser.add_option('', '--client-secrets', dest='client_secrets', + type="string", help='Client secrets JSON file') + parser.add_option('', '--credentials-file', dest='credentials_file', + type="string", help='Credentials JSON file') + parser.add_option('', '--auth-browser', dest='auth_browser', action='store_true', + help='Open a GUI browser to authenticate if required') + + #Additional options + parser.add_option('', '--chunksize', dest='chunksize', type="int", + default = 1024*1024*8, help='Update file chunksize') + parser.add_option('', '--open-link', dest='open_link', action='store_true', + help='Opens a url in a web browser to display the uploaded video') + + options, args = parser.parse_args(arguments) + + if options.description_file is not None and os.path.exists(options.description_file): + with open(options.description_file, encoding="utf-8") as file: + options.description = file.read() + + try: + run_main(parser, options, args) + except googleapiclient.errors.HttpError as error: + response = bytes.decode(error.content, encoding=lib.get_encoding()).strip() + raise RequestError(u"Server response: {0}".format(response)) + +def run(): + sys.exit(lib.catch_exceptions(EXIT_CODES, main, sys.argv[1:])) + +if __name__ == '__main__': + run() diff --git a/mods/youtube-upload/youtube_upload/playlists.py b/mods/youtube-upload/youtube_upload/playlists.py @@ -0,0 +1,53 @@ +import locale + +from .lib import debug + +def get_playlist(youtube, title): + """Return users's playlist ID by title (None if not found)""" + playlists = youtube.playlists() + request = playlists.list(mine=True, part="id,snippet") + current_encoding = locale.getpreferredencoding() + + while request: + results = request.execute() + for item in results["items"]: + t = item.get("snippet", {}).get("title") + existing_playlist_title = (t.encode(current_encoding) if hasattr(t, 'decode') else t) + if existing_playlist_title == title: + return item.get("id") + request = playlists.list_next(request, results) + +def create_playlist(youtube, title, privacy): + """Create a playlist by title and return its ID""" + debug("Creating playlist: {0}".format(title)) + response = youtube.playlists().insert(part="snippet,status", body={ + "snippet": { + "title": title, + }, + "status": { + "privacyStatus": privacy, + } + }).execute() + return response.get("id") + +def add_video_to_existing_playlist(youtube, playlist_id, video_id): + """Add video to playlist (by identifier) and return the playlist ID.""" + debug("Adding video to playlist: {0}".format(playlist_id)) + return youtube.playlistItems().insert(part="snippet", body={ + "snippet": { + "playlistId": playlist_id, + "resourceId": { + "kind": "youtube#video", + "videoId": video_id, + } + } + }).execute() + +def add_video_to_playlist(youtube, video_id, title, privacy="public"): + """Add video to playlist (by title) and return the full response.""" + playlist_id = get_playlist(youtube, title) or \ + create_playlist(youtube, title, privacy) + if playlist_id: + return add_video_to_existing_playlist(youtube, playlist_id, video_id) + else: + debug("Error adding video to playlist") diff --git a/mods/youtube-upload/youtube_upload/upload_video.py b/mods/youtube-upload/youtube_upload/upload_video.py @@ -0,0 +1,43 @@ +import socket + +try: + import httplib +except ImportError: + import http.client as httplib + +import googleapiclient.errors +import apiclient.http +import httplib2 + +from . import lib + +RETRIABLE_EXCEPTIONS = [ + socket.error, IOError, httplib2.HttpLib2Error, httplib.NotConnected, + httplib.IncompleteRead, httplib.ImproperConnectionState, + httplib.CannotSendRequest, httplib.CannotSendHeader, + httplib.ResponseNotReady, httplib.BadStatusLine, + googleapiclient.errors.HttpError, +] + +def _upload_to_request(request, progress_callback): + """Upload a video to a Youtube request. Return video ID.""" + while 1: + status, response = request.next_chunk() + if status and progress_callback: + progress_callback(status.total_size, status.resumable_progress) + if response: + if "id" in response: + return response['id'] + else: + raise KeyError("Expected field 'id' not found in response") + +def upload(resource, path, body, chunksize=4*1024*1024, + progress_callback=None, max_retries=10): + """Upload video to Youtube. Return video ID.""" + body_keys = ",".join(body.keys()) + media = apiclient.http.MediaFileUpload(path, chunksize=chunksize, + resumable=True, mimetype="application/octet-stream") + request = resource.videos().insert(part=body_keys, body=body, media_body=media) + upload_fun = lambda: _upload_to_request(request, progress_callback) + return lib.retriable_exceptions(upload_fun, + RETRIABLE_EXCEPTIONS, max_retries=max_retries) diff --git a/tarina.py b/tarina.py @@ -986,17 +986,40 @@ def copytousb(filmfolder, filmname): #-------------Upload film------------ def uploadfilm(filename, filmname): - ##SEND TO SERVER - writemessage('Hold on, video uploading. middle button to cancel') - try: - os.system('scp -P 13337 ' + filename + '.mp4 rob@tarina.org:/srv/www/tarina.org/public_html/videos/' + filmname + '.mp4') - writemessage('Yes! Film uploaded') - time.sleep(1) - except: - writemessage('hmm.. something went wrong') - time.sleep(1) - return - #os.system('ssh -t rob@lulzcam.org "python /srv/www/lulzcam.org/newfilm.py"') + pressed = '' + buttonpressed = '' + buttontime = time.time() + holdbutton = '' + mods = ['Back'] + writemessage('Searching for upload mods') + with open(tarinafolder + '/mods/upload-mods-enabled') as m: + mods.append(m.readlines()) + mods = [x.strip() for x in mods] + menu = mods + selected = 0 + while True: + header = 'Where do you want to upload?' + settings = '', '' + writemenu(menu,settings,selected,header) + pressed, buttonpressed, buttontime, holdbutton, event, keydelay = getbutton(pressed, buttonpressed, buttontime, holdbutton) + if pressed == 'right': + if selected < (len(settings) - 1): + selected = selected + 1 + elif pressed == 'left': + if selected > 0: + selected = selected - 1 + if pressed == + ##SEND TO SERVER + writemessage('Hold on, video uploading. middle button to cancel') + try: + os.system('scp -P 13337 ' + filename + '.mp4 rob@tarina.org:/srv/www/tarina.org/public_html/videos/' + filmname + '.mp4') + writemessage('Yes! Film uploaded') + time.sleep(1) + except: + writemessage('hmm.. something went wrong') + time.sleep(1) + return + #os.system('ssh -t rob@lulzcam.org "python /srv/www/lulzcam.org/newfilm.py"') #-------------Beeps------------------- @@ -1182,7 +1205,7 @@ def main(): tarinafolder = os.getcwd() #MENUS - menu = 'FILM:', 'SCENE:', 'SHOT:', 'TAKE:', '', 'SHUTTER:', 'ISO:', 'RED:', 'BLUE:', 'BRIGHT:', 'CONT:', 'SAT:', 'FLIP:', 'BEEP:', 'LENGTH:', 'MIC:', 'PHONES:', 'DUB:', 'TIMELAPSE', 'LENS:', 'DSK:', 'SHUTDOWN', 'SRV:', 'WIFI:', 'UPDATE', 'UPLOAD', 'LOAD', 'NEW' + menu = 'FILM:', 'SCENE:', 'SHOT:', 'TAKE:', '', 'SHUTTER:', 'ISO:', 'RED:', 'BLUE:', 'BRIGHT:', 'CONT:', 'SAT:', 'FLIP:', 'BEEP:', 'LENGTH:', 'MIC:', 'PHONES:', 'DUB:', 'TIMELAPSE', 'LENS:', 'DSK:', 'SHUTDOWN', 'SRV:', 'WIFI:', 'UPDATE', 'MODS', 'LOAD', 'NEW' #STANDARD VALUES global screen abc = '_', 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','1','2','3','4','5','6','7','8','9','0'