tarinaretake

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

commit 018cccbd4482890700bd5afc2fe2dd1991e0fee1
parent c52280ecf094526151e1b3450346e49b3cce7548
Author: rob <rob@tarina.org>
Date:   Mon, 13 Nov 2023 23:32:35 +0200

everything there!

Diffstat:
ADEVELOPERS.md | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AINSTALL.md | 10++++++++++
ALICENSE | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AVERSION | 2++
Aalsa-utils-1.1.3/aplay/aplay | 0
Aalsa-utils-1.1.3/aplay/arecord | 0
Acontributors.txt | 2++
Aextras/.vimrc | 34++++++++++++++++++++++++++++++++++
Aextras/bakg.jpg | 0
Aextras/bakg.xcf | 0
Aextras/beep.wav | 0
Aextras/beep_long.wav | 0
Aextras/buttons.png | 0
Aextras/buttons.xcf | 0
Aextras/debiancheck.sh | 10++++++++++
Aextras/h264streamer.py | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aextras/restorebak.sh | 25+++++++++++++++++++++++++
Aextras/sdcardhack.sh | 6++++++
Aextras/tarina.conf | 34++++++++++++++++++++++++++++++++++
Aextras/wifiset.service | 10++++++++++
Aextras/wifiset.sh | 3+++
Aextras/youtubelive.sh | 1+
Aextras/ytstream.py | 25+++++++++++++++++++++++++
Agui/VeraMono.ttf | 0
Agui/tarinagui.bin | 0
Ainstall.sh | 369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alenses/cs6mm | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amods/collab-edit.sh | 7+++++++
Amods/collab-pull.sh | 7+++++++
Amods/collab-push.sh | 7+++++++
Amods/install-youtube-upload.sh | 20++++++++++++++++++++
Amods/tarina-upload.sh | 7+++++++
Amods/upload-mods-enabled | 5+++++
Amods/youtube-upload.sh | 44++++++++++++++++++++++++++++++++++++++++++++
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 | 23+++++++++++++++++++++++
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+++++++++++++++++++++++++++++++++++++++++++
Asrv/static/Videos | 2++
Asrv/static/style.css | 22++++++++++++++++++++++
Asrv/tarinaserver.py | 241+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrv/tarinaserver.pyc | 0
Asrv/templates/base.html | 12++++++++++++
Asrv/templates/filmpage.html | 39+++++++++++++++++++++++++++++++++++++++
Asrv/templates/index.html | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astartinterface.sh | 5+++++
63 files changed, 2694 insertions(+), 0 deletions(-)

diff --git a/DEVELOPERS.md b/DEVELOPERS.md @@ -0,0 +1,90 @@ +DEVELOPERS +========== + +If you want to join me in this journey to make the best open filmmaking camera please do not hesitate to contact me on features. + +Teh best way to develop is using screen in terminal and starting tarina in one window then you'll get debug messages directly in that window. + +THINGS +====== + +Notes to myself as I'm the only developer at teh moment. +-Rob + +Quality / Usability +------------------- + +Found a sweet spot between usability and quality (23), bitrate (5 mb/s) +This produces about 25 mb per minute. The convertion to mp4 format does not feel so slow anymore likewise the compiling of the film. This is good stuff. + +Image sensors +------------- + +There's plenty of image sensors for the Raspberry Pi to choose from, I want to support as many sensors I can. Currently though you need to change your sensor in tarina.py, search for 'ov5' and you'll find where to select. They all have different oscillators so the framerate varies pretty much. But when you get it right the sound will be in sync. Will make a tarina config file in home folder so when you start tarina interface for the first time it will ask you about your sensor. + +Cancel renderer +--------------- + +This is a must feature, and I sort of know how to implement it. + +Viewfinder for 3.5 inch screen +------------------------------ + +There's no such thing to buy so I had to make a viewfinder myself, took a while before I found the right convex lens but I have it now. It works with magnet snap on. Really, really usefull! will write it in the manual as a addon thing... + +How to update alsa-tools arecord +================================= + +The vumeter comes directly from alsa-tools arecord, it's just modified so it writes the vumeter directly to /dev/shm/vumeter. If you need to update alsa-tools here's how: + +You need to be able to build alsa-utils from source +enable dev-src in /etc/apt/sources.list + +``` +apt source alsa-utils +cd alsa-utils* +sudo apt update +sudo apt install libncurses5-dev +sudo apt install libasound2-dev +./configure +make +cd aplay +make +make arecord +``` + + +Ecasound update +=============== + +### Python example + +``` +from pyeca import * +e = ECA_CONTROL_INTERFACE() +e.command("cs-add chainsetup") +e.command("c-add 1st_chain") +e.command("ai-add plughw,0,0 ") +e.command("ao-add /dev/dsp") +e.command("cop-add -efl:100") +e.command("cop-select 1") +e.command("copp-select 1") +e.command("cs-connect") +e.command("start") +``` + +### Ecasound config file + +``` +# ecasound chainsetup file + +# general +-B:rtlowlatency -n:"write_chain" -X -z:noxruns -z:nopsr -z:mixmode,sum + +# audio inputs +-a:1chain,2chain -f:s16_le,2,44100 -i:alsahw,3,0,0 + +# audio outputs +-a:1chain -f:s16_le,2,44100 -o:alsahw,3,0,0 +-a:2chain -f:s16_le,2,44100 -o:test.wav, +``` diff --git a/INSTALL.md b/INSTALL.md @@ -0,0 +1,10 @@ +## Tarina install instructions ## + +Run the automatic install script with sudo: +``` +sudo ./install.sh +``` +you are done! now reboot & Tarina will start at bootup: +``` +sudo reboot +``` diff --git a/LICENSE b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md @@ -0,0 +1,112 @@ +Tarina Retake +============= +The Filmmaking Device + +Let's introduce this thing with a thing it made, [a film on youtube](https://youtu.be/Yl2oAxMtDV0?si=lXOYTpkJi1YFuO2u) ! + +![Two buddies](docs/tarina-promo.jpg) + +Hollywood in your palms +-------------------------- +Retake shots on the spot and see movie making magic in the filmmaking interface that runs Tarina Retake. + +Software +-------- +A video camera with *most of* the tools to make a film within the camera. That means alot of features. So far we have these key features running. +- glue the selected clips together and/or cutting them. +- making timelapses, voiceover, music track recording, slo-mo recording, fast-forward recording +- cut and copy and move clips around +- backup to usb harddrive or your own server +- upload or stream to youtube or your own server +- auto correction can easily be switched on or off for shutter, iso and colors so *operator* is in full control also for audio levels +- connect many Tarinas together for multicamera shooting +- stream a film a take or a scene through the network +- control the camera with silent physical buttons or a usb-wireless-keyboard or through https or ssh or ports, you choose. + +It's all in there. But, where? in the filmmaking interface, that is Tarina. + +Hardware +-------- +The parts have been chosen on the basis of features, quality, openness, availabilty and price. One of the key ideas of the project is to have a camera that could be upgraded or repaired by the fact that you easily just switch a component. The casis of the camera is 3d printable with a flipping gonzo style 180 shooting mode, please take a look [here to get the picture](https://github.com/rbckman/tarina/tree/master/3d) + +### Buttons +![Buttons](docs/buttons.png) + +Here's the main components: + +[Raspberry Pi 3 B](https://www.raspberrypi.org/products/raspberry-pi-3-model-b/)<br> +[Raspberry Pi High Quality Camera](https://www.raspberrypi.org/products/raspberry-pi-high-quality-camera/?resellerType=home) or <br> +[Pimoroni Hyperpixel 4 inch screen](https://learn.pimoroni.com/tutorial/sandyj/getting-started-with-hyperpixel-4) +[USB via vt1620a Sound card](https://www.aliexpress.com/item/Professional-External-USB-Sound-Card-Adapter-Virtual-7-1-Channel-3D-Audio-with-3-5mm-Headset/32588038556.html?spm=2114.01010208.8.8.E8ZKLB)<br> +[3.7v 7800mAh li-ion Battery](https://www.aliexpress.com/item/3-7v-9000mAh-capacity-18650-Rechargeable-lithium-battery-pack-18650-jump-starter/32619902319.html?spm=2114.13010608.0.0.XcKleV)<br> +[Type-C 5v 2A 3.7V Li-ion battery charger booster module](https://www.ebay.com/itm/Type-C-USB-5V-2A-3-7V-18650-Lithium-Li-ion-Battery-Charging-Board-Charger-Module/383717339632?var=652109038482) +[Buttons](http://www.ebay.com/itm/151723036469?_trksid=p2057872.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT) connected to a [MCP23017-E/SP DIP-28 16 Bit I / O Expander I2C](http://www.ebay.com/sch/sis.html?_nkw=5Pcs+MCP23017+E+SP+DIP+28+16+Bit+I+O+Expander+I2C+TOP+GM&_trksid=p2047675.m4100) + +Check [MANUAL](docs/tarina-manual.md) for complete part list & build instructions + +[Ready to print 3d designs](https://github.com/rbckman/tarina/tree/master/3d) + +Installing +---------- +Download [Raspbian buster (not the latest!)](https://www.raspberrypi.org/downloads/raspbian/) and follow [install instructions | a simple install script should take care of it all!](https://www.raspberrypi.org/documentation/installation/installing-images/README.md). +[Ssh into](https://www.raspberrypi.org/documentation/remote-access/ssh/) Raspberry Pi and run: +``` +sudo apt-get install git +``` +Go to /home/pi/ folder +``` +cd /home/pi +``` +Git clone tarina and then run install script with sudo: +``` +git clone https://github.com/rbckman/tarina.git +cd tarina +sudo ./install.sh +``` +You'r ready to rumble: +``` +python3 tarina.py +``` + +Why +--- +There are several reasons why. + +- be able to repair if something breaks (this has been prooven as a very nice feature) +- be able to expand / build on it / make modifications +- be able to connect to it / program it to do things +- do a film on the fly without the need of another computer +- be able to watch your film directly on a screen once you're done filming +- learn about programming and your own crafts to really get down to the nitty-gritty. + +Connect +------- +Matrix [#tarina:matrix.tarina.org](https://riot.im/app/#/room/#tarina:matrix.tarina.org) + +Mail rob(at)tarina.org + +Standing on the shoulders of forward thinking, freedom loving generous people (powa to da people!) +-------------------------------------------------------------------------------------------------- +This whole project has only been possible because of the people behind the free and open source movement. Couldn't possible list all of the projects on which shoulders this is standing for it would reach the moon. A big shout out to all of ya!! Yall awesome! + +[Gnu](https://gnu.org), [Linux](https://github.com/torvalds/linux), [Debian](https://debian.org), [Raspberry Pi](https://raspberrypi.org), +[Python programming language](https://python.org), Dave Jones's [Picamera python module](https://github.com/waveform80/picamera), rwb27 for lens shading correction! Check out the 3d printable microscope [Openflexure](https://github.com/rwb27/openflexure_microscope), [FFmpeg](https://ffmpeg.org/), [Libav-tools](https://libav.org/), [GPac library with MP4Box](https://gpac.wp.imt.fr/mp4box/), [Blender](http://blender.org), [aplay the awesome wav player/recorder with VU meter](http://alsa.opensrc.org/Aplay), [Popcornmix's Omxplayer](https://github.com/popcornmix/omxplayer), [Will Price's Python-omxplayer-wrapper](https://github.com/willprice/python-omxplayer-wrapper), [SoX - Sound eXchange](http://sox.sourceforge.net/), [The Dispmanx library](https://github.com/raspberrypi/userland/tree/master/host_applications/linux/apps/hello_pi), [Blessed](http://blessed.readthedocs.io/), [web.py](http://webpy.org), [Tokland's youtube-upload](https://github.com/tokland/youtube-upload) + +![Tarina and Leon](docs/tarina-filming-01.jpg) + +Some films made with Tarina +---------------------- + +### [Aakenustunturi Hiihtoretki 2023](https://youtu.be/VB7R2Eiw13k) + +### [Mancherok](https://youtu.be/jmy0W6rA10Q) + +### [Robins Trägård](https://youtu.be/IOZAHCIN6U0) + +### [A new years medley](https://youtu.be/BYojmnD-1eU) + +### [Landing Down Under](https://www.youtube.com/watch?v=Lbi9_f0KrKA) + +### [Building Tarina](https://youtu.be/7dhCiDPssR4) + +### [Mushroom Season](https://youtu.be/ggehzyUThZk) diff --git a/VERSION b/VERSION @@ -0,0 +1,2 @@ +1.42 +Jackson diff --git a/alsa-utils-1.1.3/aplay/aplay b/alsa-utils-1.1.3/aplay/aplay Binary files differ. diff --git a/alsa-utils-1.1.3/aplay/arecord b/alsa-utils-1.1.3/aplay/arecord Binary files differ. diff --git a/contributors.txt b/contributors.txt @@ -0,0 +1,2 @@ +Robin Bäckman +Robin Bäckman diff --git a/extras/.vimrc b/extras/.vimrc @@ -0,0 +1,34 @@ +" File ~/.vim/ftplugin/html.vim + +set cursorline +set encoding=utf-8 +set fileencoding=utf-8 +set mouse=a +set showcmd +syntax enable +filetype indent on +set nobackup +set number +set ruler +set undolevels=1000 +set backspace=indent,eol,start +set tabstop=8 +set expandtab +set softtabstop=4 +set shiftwidth=4 +set nobackup +set nowb +set noswapfile +set linebreak +colorscheme desert +set background=dark +nnoremap j gj +nnoremap k gk +vnoremap j gj +vnoremap k gk +nnoremap <Down> gj +nnoremap <Up> gk +vnoremap <Down> gj +vnoremap <Up> gk +inoremap <Down> <C-o>gj +inoremap <Up> <C-o>gk diff --git a/extras/bakg.jpg b/extras/bakg.jpg Binary files differ. diff --git a/extras/bakg.xcf b/extras/bakg.xcf Binary files differ. diff --git a/extras/beep.wav b/extras/beep.wav Binary files differ. diff --git a/extras/beep_long.wav b/extras/beep_long.wav Binary files differ. diff --git a/extras/buttons.png b/extras/buttons.png Binary files differ. diff --git a/extras/buttons.xcf b/extras/buttons.xcf Binary files differ. diff --git a/extras/debiancheck.sh b/extras/debiancheck.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Basic if statement +version="$(lsb_release -c -s)" +echo $version +if [ "$version" = "buster" ] +then + echo "Debian Buster found" +else + echo "Debian Stretch found" +fi diff --git a/extras/h264streamer.py b/extras/h264streamer.py @@ -0,0 +1,64 @@ +import io +import picamerax as picamera +import time +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from wsgiref.simple_server import make_server +from ws4py.websocket import WebSocket +from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIHandler, WebSocketWSGIRequestHandler +from ws4py.server.wsgiutils import WebSocketWSGIApplication +from threading import Thread, Condition + + +class FrameBuffer(object): + def __init__(self): + self.frame = None + self.buffer = io.BytesIO() + self.condition = Condition() + + def write(self, buf): + if buf.startswith(b'\x00\x00\x00\x01'): + with self.condition: + self.buffer.seek(0) + self.buffer.write(buf) + self.buffer.truncate() + self.frame = self.buffer.getvalue() + self.condition.notify_all() + + +def stream(): + with picamera.PiCamera(resolution='1920x816', framerate=25) as camera: + broadcasting = True + frame_buffer = FrameBuffer() + camera.start_recording(frame_buffer, format='h264', profile="baseline") + try: + WebSocketWSGIHandler.http_version = '1.1' + websocketd = make_server('', 9000, server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=WebSocket)) + websocketd.initialize_websockets_manager() + websocketd_thread = Thread(target=websocketd.serve_forever) + + httpd = ThreadingHTTPServer(('', 8000), SimpleHTTPRequestHandler) + httpd_thread = Thread(target=httpd.serve_forever) + + try: + websocketd_thread.start() + httpd_thread.start() + while broadcasting: + with frame_buffer.condition: + frame_buffer.condition.wait() + websocketd.manager.broadcast(frame_buffer.frame, binary=True) + except KeyboardInterrupt: + pass + finally: + websocketd.shutdown() + httpd.shutdown() + broadcasting = False + raise KeyboardInterrupt + except KeyboardInterrupt: + pass + finally: + camera.stop_recording() + +if __name__ == "__main__": + stream() diff --git a/extras/restorebak.sh b/extras/restorebak.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +ROOT_UID=0 # Root has $UID 0. + +if [ "$UID" -eq "$ROOT_UID" ] +then + echo "OK" +else + echo "Run with sudo!" + echo "sudo ./restorebak.sh" + exit 0 +fi + +while true; do + read -p "Undo rpi-update? [y]es or [n]o?" yn + case $yn in + [Yy]* ) echo "Restoring from backup now..." +cp -r /boot.bak/* /boot/ +cp -r /lib/modules.bak/* /lib/modules/ + break;; + [Nn]* ) echo "Yes, sir! we are done!";break;; + * ) echo "Please answer yes or no.";; + esac +done + diff --git a/extras/sdcardhack.sh b/extras/sdcardhack.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo 3 >/proc/sys/vm/dirty_background_ratio +echo 50 >/proc/sys/vm/dirty_ratio +echo 300 >/proc/sys/vm/dirty_writeback_centisecs +echo 300 >/proc/sys/vm/dirty_expire_centisecs diff --git a/extras/tarina.conf b/extras/tarina.conf @@ -0,0 +1,34 @@ +<VirtualHost *:80> + ServerAdmin info@tarina.org + WSGIScriptAlias / /home/pi/tarina/srv/tarinaserver.py + WSGIPassAuthorization On + AddType text/html .py + Alias /static /home/pi/tarina/srv/static + <Directory /> + Options FollowSymLinks + AllowOverride None + </Directory> + <Directory /home/pi/srv> + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + </Directory> + + ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ + <Directory "/usr/lib/cgi-bin"> + AllowOverride None + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + Order allow,deny + Allow from all + </Directory> + + ErrorLog ${APACHE_LOG_DIR}/error.log + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + CustomLog ${APACHE_LOG_DIR}/access.log combined + +</VirtualHost> diff --git a/extras/wifiset.service b/extras/wifiset.service @@ -0,0 +1,10 @@ +[Unit] +Description=Set wifi localization +After=multi-user.target + +[Service] +Type=idle +ExecStart=/home/pi/tarina/extras/wifiset.sh + +[Install] +WantedBy=multi-user.target diff --git a/extras/wifiset.sh b/extras/wifiset.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo 'Setting your wifi region' +sudo iw reg set FI diff --git a/extras/youtubelive.sh b/extras/youtubelive.sh @@ -0,0 +1 @@ +raspivid -o - -t 0 -w 1920 -h 816 -vf -hf -fps 25 -b 3000000 | ffmpeg -re -ar 44100 -acodec pcm_s16le -f alsa -ac 1 -i hw:0 -f h264 -i - -vcodec copy -acodec aac -ab 128k -g 50 -strict experimental -f flv rtmp://a.rtmp.youtube.com/live2/j1cs-7hac-ygrj-5gaq-3m5b diff --git a/extras/ytstream.py b/extras/ytstream.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import subprocess +import picamerax +import time +YOUTUBE="rtmp://a.rtmp.youtube.com/live2/" +KEY='j1cs-7hac-ygrj-5gaq-3m5b' +stream_cmd = 'ffmpeg -f h264 -r 25 -i - -itsoffset 5.5 -fflags nobuffer -f alsa -ac 1 -i hw:0 -vcodec copy -acodec libmp3lame -ar 44100 -map 0:0 -map 1:0 -strict experimental -f flv ' + YOUTUBE + KEY +stream = subprocess.Popen(stream_cmd, shell=True, stdin=subprocess.PIPE) +camera = picamerax.PiCamera(resolution=(640, 480), framerate=25) +try: + now = time.strftime("%Y-%m-%d-%H:%M:%S") + camera.framerate = 25 + camera.vflip = True + camera.hflip = True + camera.start_recording(stream.stdin, format='h264', bitrate = 2000000) + while True: + camera.wait_recording(1) +except KeyboardInterrupt: + camera.stop_recording() +finally: + camera.close() + stream.stdin.close() + stream.wait() + print("Camera safely shut down") + print("Good bye") diff --git a/gui/VeraMono.ttf b/gui/VeraMono.ttf Binary files differ. diff --git a/gui/tarinagui.bin b/gui/tarinagui.bin Binary files differ. diff --git a/install.sh b/install.sh @@ -0,0 +1,369 @@ +#!/bin/bash +#sed -i '/FILESYSTEMS=/c\FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs fuseblk vfat"' /etc/usbmount/usbmount.conf + +ROOT_UID=0 # Root has $UID 0. + +update=$1 + +if [ "$UID" -eq "$ROOT_UID" ] +then + echo "OK" +else + echo "Run with sudo!" + echo "sudo ./install.sh" + exit 0 +fi + +echo "Hurray, you are root! Let's do this.." +cat <<'EOF' + + _______ _____ _____ _ _ + |__ __|/\ | __ \|_ _| \ | | /\ + | | / \ | |__) | | | | \| | / \ + | | / /\ \ | _ / | | | . ` | / /\ \ + | |/ ____ \| | \ \ _| |_| |\ |/ ____ \ + |_/_/ \_\_| \_\_____|_| \_/_/ \_\ + + +-+ +-+-+-+-+-+-+ +-+-+ +-+-+-+-+-+-+-+-+-+-+ + |a| |r|e|t|a|k|e| |o|n| |f|i|l|m|m|a|k|i|n|g| + +-+ +-+-+-+-+-+-+ +-+-+ +-+-+-+-+-+-+-+-+-+-+ + +EOF +sleep 1 + +if grep -q -F '#tarina-rpi-configuration-1.0' /boot/config.txt +then +echo "screen drivers found! remove them in /boot/config.txt" +else +echo "Select screen driver to be installed" +select screen in hyperpixel4 ugeek-hdtft +do +echo $screen +break +done +fi +echo "setting up system for filmmaking flow..." +echo "if something goes wrong please submit bug to https://github.com/rbckman/tarina" +sleep 2 +version="$(lsb_release -c -s)" +if [ "$version" = "buster" ] +then + echo "Debian Buster found" +else + echo "Debian Stretch found" +fi +echo "Installing all dependencies..." + +apt-get update +apt-get upgrade -y +if [ "$version" = "buster" ] +then + apt-get -y install git python3-pip python-configparser ffmpeg mediainfo gpac omxplayer sox cpufrequtils apache2 libapache2-mod-wsgi-py3 libdbus-glib-1-dev dbus libdbus-1-dev usbmount python3-numpy python3-pil python3-smbus python3-shortuuid wiringpi make gcc cmake pmount python3-ifaddr +else + apt-get -y install git python3-pip python-configparser libav-tools mediainfo gpac omxplayer sox cpufrequtils apache2 libapache2-mod-wsgi-py3 libdbus-glib-1-dev dbus libdbus-1-dev usbmount python3-numpy python3-pil python3-smbus python3-shortuuid wiringpi make gcc cmake python3-ifaddr +fi +echo "installing python-omxplayer-wrapper..." +sudo pip3 install omxplayer-wrapper +echo "installing blessed..." +sudo pip3 install blessed +echo "installing secrets..." +sudo pip3 install secrets +sudo pip3 install numpy +sudo pip3 install RPi.GPIO +echo "installing picamerax with lens shading correction..." +#sudo pip3 --no-cache-dir install https://github.com/chrisruk/picamera/archive/hq-camera-new-framerates.zip --upgrade +sudo pip3 install --upgrade picamerax +echo "installing web.py for the tarina webserver..." +sudo pip3 install web.py==0.61 + +if [ "$screen" = "ugeek-hdtft" ] +then +echo "installing ugeek screen drivers" +echo "Tarina configuration seems to be in order in /boot/config.txt" +echo "Adding to /boot/config.txt" +cat <<'EOF' >> /boot/config.txt +#-----Tarina configuration starts here------- +#tarina-rpi-configuration-ugeek-1.0 +#Rpi-hd-tft +dtoverlay=dpi18 +overscan_left=0 +overscan_right=0 +overscan_top=0 +overscan_bottom=0 +framebuffer_width=800 +framebuffer_height=480 +enable_dpi_lcd=1 +display_default_lcd=1 +dpi_group=2 +dpi_mode=87 +dpi_output_format=0x6f015 +hdmi_timings=480 0 16 16 24 800 0 4 2 2 0 0 0 60 0 32000000 6 +dtoverlay=pi3-disable-bt-overlay +dtoverlay=i2c-gpio,i2c_gpio_scl=24,i2c_gpio_sda=23framebuffer_height=480 +display_rotate=3 +start_x=1 +gpu_mem=256 +disable_splash=1 +force_turbo=1 +boot_delay=1 +dtparam=i2c_arm=on +# dtparam=sd_overclock=90 +# Disable the ACT LED. +dtparam=act_led_trigger=none +dtparam=act_led_activelow=off +# Disable the PWR LED. +dtparam=pwr_led_trigger=none +dtparam=pwr_led_activelow=off + +#--------Tarina configuration end here--------- +EOF +elif [ "$screen" = "hyperpixel4" ] +then +apt-get -y install curl +echo "installing hyperpixel4 screen drivers" +curl -sSL get.pimoroni.com/hyperpixel4-legacy | bash +cat <<'EOF' >> /etc/udev/rules.d/98-hyperpixel4-calibration.rules +ATTRS{name}=="Goodix Capacitive TouchScreen", ENV{LIBINPUT_CALIBRATION_MATRIX}="0 1 0 -1 0 1" +EOF +echo "Tarina configuration seems to be in order in /boot/config.txt" +echo "Adding to /boot/config.txt" +cat <<'EOF' >> /boot/config.txt +#-----Tarina configuration starts here------- +#tarina-rpi-configuration-hyperpixel-1.0 +#hyperpixel +start_x=1 +gpu_mem=256 +disable_splash=1 +force_turbo=1 +boot_delay=1 +# dtparam=sd_overclock=90 +# Disable the ACT LED. +dtparam=act_led_trigger=none +dtparam=act_led_activelow=off +# Disable the PWR LED. +dtparam=pwr_led_trigger=none +dtparam=pwr_led_activelow=off +framebuffer_width=800 +framebuffer_height=480 +#hdmi_force_hotplug=1 +hdmi_group=1 +hdmi_mode=3 +[EDID=N/A-] ##Hyperpixel HD CONFIG + +dtoverlay=hyperpixel4 +overscan_left=0 +overscan_right=0 +overscan_top=0 +overscan_bottom=0 +enable_dpi_lcd=1 +display_default_lcd=1 +display_rotate=1 +dpi_group=2 +hdmi_group=2 +dpi_mode=87 +dpi_output_format=0x7f216 +hdmi_timings=480 0 10 16 59 800 0 15 113 15 0 0 0 60 0 32000000 6 + +#--------Tarina configuration end here--------- +EOF +else +echo "screen driver already there, to change it remove tarina config in /boot/config.txt" +fi + +echo "Change hostname to tarina" +cat <<'EOF' > /etc/hostname +tarina +EOF + +cat <<'EOF' > /etc/hosts +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + +127.0.1.1 tarina +EOF + +echo "consoleblank=0 logo.nologo loglevel=0" +echo "may be put at the end of line in this file /boot/cmdline.txt" +sleep 4 + +echo "Make USB soundcard default" +echo "writing to /etc/modprobe.d/alsa-base.conf" +if [ "$version" = "buster" ] +then +echo "Debian Buster Alsa config" +cat <<'EOF' > /etc/modprobe.d/alsa-base.conf +#set index value +options snd-usb-audio index=0 +options snd_bcm2835 index=1 +#reorder +options snd slots=snd_usb_audio, snd_bcm2835 +EOF +else +echo "Debian Stretch Alsa config" +cat <<'EOF' > /etc/modprobe.d/alsa-base.conf +#set index value +options snd_usb_audio index=0 +options snd_bcm2835 index=1 +#reorder +options snd slots=snd_usb_audio, snd_bcm2835 +EOF +fi + +echo "Automatically boot to Tarina" +echo "creating a tarina.service file" +cat <<'EOF' > /etc/systemd/system/tarina.service +[Unit] +Description=tarina +After=getty.target +Conflicts=getty@tty1.service +#DefaultDependencies=false + +[Service] +Type=simple +RemainAfterExit=yes +ExecStart=/usr/bin/python3 /home/pi/tarina/tarina.py default +User=pi +Restart=on-failure +StandardInput=tty-force +StandardOutput=inherit +StandardError=inherit +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes +Nice=-20 +CPUSchedulingPolicy=rr +CPUSchedulingPriority=99 + +[Install] +WantedBy=local-fs.target +EOF + +#dont kill process if user log out or in + +cat <<'EOF' >> /etc/systemd/logind.conf +KillUserProcesses=no +EOF + + +#thanx systemd for making me search for years to make this all workd like a normal programd. +loginctl enable-linger +loginctl enable-linger pi + +chmod +x /home/pi/tarina/tarina.py +systemctl enable tarina.service +systemctl daemon-reload +echo "systemd configuration done!" + +echo "Installing tarina apache server configuration" +cp extras/tarina.conf /etc/apache2/sites-available/ +#ln -s -t /var/www/ /home/pi/tarina/srv/ +a2dissite 000-default.conf +a2ensite tarina.conf +echo "configure srv path to /home/pi/tarina/srv" + +cat <<'EOF' >> /etc/apache2/apache2.conf +<Directory /home/pi/tarina/srv> + Options Indexes FollowSymLinks + AllowOverride None + Require all granted +</Directory> +EOF +systemctl reload apache2 + +echo 'Dont do sync while copying to usb drives, does increase speed alöt!' +sed -i '/MOUNTOPTIONS=/c\MOUNTOPTIONS="noexec,nodev,noatime,nodiratime"' /etc/usbmount/usbmount.conf + +echo "Adding harddrive tools..." +cat <<'EOF' +All this hard work to figure out how to keep NTFS mounted was done by F. Untermoser +https://raspberrypi.stackexchange.com/questions/41959/automount-various-usb-stick-file-systems-on-jessie-lite +Thanks alot! + +while we are at it :) +To all the amazing FOSS people out there big big props and + _____ ______ _____ _____ ______ _____ _______ _ + | __ \| ____|/ ____| __ \| ____/ ____|__ __| | + | |__) | |__ | (___ | |__) | |__ | | | | | | + | _ /| __| \___ \| ___/| __|| | | | | | + | | \ \| |____ ____) | | | |___| |____ | | |_| + |_| \_\______|_____/|_| |______\_____| |_| (_) + +EOF +apt-get -y install ntfs-3g exfat-fuse +#sed -i -e 's/MountFlags=slave/MountFlags=shared/g' /lib/systemd/system/systemd-udevd.service +#sed -i '/FS_MOUNTOPTIONS=/c\FS_MOUNTOPTIONS="-fstype=ntfs-3g,nls=utf8,umask=007,gid=46 -fstype=fuseblk,nls=utf8,umask=007,gid=46 -fstype=vfat,gid=1000,uid=1000,umask=007"' /etc/usbmount/usbmount.conf +#sed -i '/FILESYSTEMS=/c\FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs fuseblk vfat"' /etc/usbmount/usbmount.conf + +cat <<'EOF' >> /etc/usbmount/usbmount.conf +FS_MOUNTOPTIONS="-fstype=ntfs-3g,nls=utf8,umask=007,gid=46 -fstype=fuseblk,nls=utf8,umask=007,gid=46 -fstype=vfat,gid=1000,uid=1000,umask=007" +FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs fuseblk vfat" +EOF + +cat <<'EOF' > /etc/udev/rules.d/usbmount.rules +KERNEL=="sd*", DRIVERS=="sbp2", ACTION=="add", PROGRAM="/bin/systemd-escape -p --template=usbmount@.service $env{DEVNAME}", ENV{SYSTEMD_WANTS}+="%c" +KERNEL=="sd*", SUBSYSTEMS=="usb", ACTION=="add", PROGRAM="/bin/systemd-escape -p --template=usbmount@.service $env{DEVNAME}", ENV{SYSTEMD_WANTS}+="%c" +KERNEL=="ub*", SUBSYSTEMS=="usb", ACTION=="add", PROGRAM="/bin/systemd-escape -p --template=usbmount@.service $env{DEVNAME}", ENV{SYSTEMD_WANTS}+="%c" +KERNEL=="sd*", ACTION=="remove", RUN+="/usr/share/usbmount/usbmount remove" +KERNEL=="ub*", ACTION=="remove", RUN+="/usr/share/usbmount/usbmount remove" +EOF + +cat <<'EOF' > /etc/systemd/system/usbmount@.service +[Unit] +BindTo=%i.device +After=%i.device + +[Service] +Type=oneshot +TimeoutStartSec=0 +Environment=DEVNAME=%I +ExecStart=/usr/share/usbmount/usbmount add +RemainAfterExit=yes +EOF + +echo "Adding hacking tools..." +apt-get -y install vim htop screen nmap +cp extras/.vimrc /root/.vimrc +cp extras/.vimrc /home/pi/.vimrc + +echo "Installing youtube upload mod..." +pip3 install pyshorteners +pip3 install google-api-python-client==1.7.3 oauth2client==4.1.2 progressbar2==3.38.0 httplib2==0.15.0 + +cd mods +./install-youtube-upload.sh +cd .. + +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 +apt-get -y install wicd wicd-curses + +echo "Removing unnecessary programs from startup..." +systemctl disable lightdm.service --force +systemctl disable graphical.target --force +systemctl disable plymouth.service --force +systemctl disable bluetooth.service +systemctl disable hciuart.service + +echo "Configure wifi region settings to FI, finland" +echo "You can change settings in extras/wifiset.sh file" +cp extras/wifiset.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable wifiset.service + +echo "HURRAY! WE ARE" +cat <<'EOF' + _____ ____ _ _ ______ _ + | __ \ / __ \| \ | | ____| | + | | | | | | | \| | |__ | | + | | | | | | | . ` | __| | | + | |__| | |__| | |\ | |____|_| + |_____/ \____/|_| \_|______(_) + +EOF +sleep 2 +echo "Rebooting into up-to-date Tarina..." +sleep 2 +reboot diff --git a/lenses/cs6mm b/lenses/cs6mm @@ -0,0 +1,133 @@ +uint8_t ls_grid[] = { +//R - Ch 3 +44, 43, 42, 42, 41, 40, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 37, 37, +44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, +43, 42, 42, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 37, +43, 42, 41, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 36, 36, +42, 42, 41, 40, 39, 39, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, +42, 41, 41, 40, 39, 39, 38, 38, 38, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, +42, 41, 40, 39, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, +42, 41, 40, 40, 39, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, +41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, +41, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, +41, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, +40, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +41, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, +41, 40, 39, 39, 38, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, +41, 41, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, +41, 41, 40, 39, 39, 38, 38, 38, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, +41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, +41, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 33, 33, 33, 33, 33, 34, 34, +42, 41, 41, 40, 39, 39, 39, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, +42, 42, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, +43, 42, 41, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 37, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 36, +43, 42, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 36, 36, +44, 43, 42, 41, 41, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, +44, 43, 42, 42, 41, 41, 41, 41, 39, 38, 38, 37, 37, 37, 37, 36, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, +45, 44, 43, 42, 41, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 37, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 37, +45, 44, 43, 43, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 37, 37, 38, +46, 45, 44, 44, 42, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 37, 36, 36, 35, 35, 36, 35, 35, 35, 36, 36, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 38, 38, 38, 38, +//Gr - Ch 2 +52, 51, 50, 49, 48, 47, 46, 45, 45, 44, 43, 43, 43, 42, 42, 41, 41, 41, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 44, 45, 45, 46, 46, +52, 50, 49, 48, 47, 47, 46, 45, 44, 44, 43, 42, 42, 42, 41, 41, 40, 40, 40, 40, 39, 39, 39, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 43, 43, 44, 45, 45, 46, 46, +51, 50, 49, 48, 47, 46, 46, 45, 44, 43, 42, 42, 41, 41, 40, 40, 40, 39, 39, 39, 39, 39, 38, 38, 39, 39, 39, 39, 40, 40, 40, 41, 41, 42, 42, 43, 44, 44, 45, 46, 45, +50, 49, 49, 47, 46, 46, 46, 45, 44, 42, 42, 41, 40, 40, 40, 39, 39, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 43, 43, 44, 44, 45, 45, +50, 49, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 39, 39, 40, 40, 41, 41, 42, 43, 44, 44, 45, 45, +50, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 38, 39, 39, 39, 40, 41, 42, 43, 43, 44, 45, 44, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 44, 44, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 35, 35, 34, 35, 35, 35, 35, 36, 36, 37, 37, 38, 39, 39, 40, 41, 42, 42, 43, 44, 44, +49, 48, 46, 45, 44, 43, 43, 41, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 43, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 34, 33, 33, 34, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 39, 41, 41, 42, 43, 43, 44, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 43, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 34, 33, 33, 32, 32, 32, 33, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 45, 44, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 43, +48, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 45, 43, 42, 41, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 43, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 33, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 43, 43, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 33, 33, 32, 33, 33, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +49, 48, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 44, 43, +49, 48, 47, 46, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 37, 38, 39, 39, 40, 41, 42, 43, 44, 44, +49, 48, 47, 46, 44, 43, 42, 42, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 44, 43, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 44, +50, 49, 47, 46, 45, 44, 43, 42, 42, 41, 40, 39, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 39, 40, 41, 42, 43, 44, 44, 44, +50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 41, 40, 39, 39, 39, 38, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 38, 38, 39, 39, 40, 41, 41, 42, 43, 44, 44, 45, +51, 50, 48, 47, 47, 46, 44, 44, 43, 42, 41, 41, 40, 40, 39, 39, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 39, 39, 40, 40, 41, 42, 43, 43, 44, 45, 45, +51, 50, 49, 48, 47, 46, 46, 44, 43, 43, 42, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 41, 42, 42, 43, 44, 45, 45, 46, +52, 51, 50, 49, 47, 48, 48, 46, 44, 43, 43, 42, 41, 41, 40, 40, 39, 39, 39, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 42, 43, 44, 44, 45, 46, 46, +53, 51, 50, 49, 48, 48, 48, 47, 45, 44, 43, 43, 42, 41, 41, 41, 40, 40, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 43, 44, 44, 45, 46, 46, 46, +53, 52, 51, 49, 48, 48, 48, 46, 45, 45, 44, 43, 43, 42, 42, 42, 41, 41, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 44, 44, 45, 46, 46, 47, 46, +53, 52, 51, 50, 49, 48, 47, 47, 46, 45, 45, 44, 43, 43, 43, 42, 42, 42, 42, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 44, 44, 45, 46, 47, 47, 47, 47, +53, 52, 51, 51, 50, 49, 48, 48, 47, 47, 46, 46, 45, 44, 44, 44, 44, 43, 43, 43, 42, 43, 43, 43, 43, 43, 43, 43, 43, 44, 44, 45, 45, 45, 45, 46, 47, 48, 48, 49, 47, +//Gb - Ch 1 +52, 51, 50, 49, 48, 47, 46, 45, 44, 44, 43, 42, 42, 42, 41, 41, 40, 40, 40, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 41, 41, 42, 42, 43, 43, 43, 44, 45, 46, 46, 46, +52, 50, 49, 48, 47, 46, 46, 45, 44, 43, 43, 42, 42, 41, 41, 40, 40, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 41, 41, 42, 42, 43, 43, 44, 45, 46, 46, 46, +51, 50, 49, 48, 47, 46, 46, 45, 44, 43, 42, 42, 41, 41, 40, 40, 39, 39, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 42, 42, 43, 44, 45, 45, 46, 46, +51, 50, 48, 47, 46, 46, 46, 45, 44, 42, 42, 41, 40, 40, 39, 39, 38, 38, 38, 37, 37, 37, 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 43, 44, 44, 45, 45, 46, +50, 49, 48, 47, 46, 45, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 39, 39, 40, 40, 41, 42, 42, 43, 44, 44, 45, 45, +50, 49, 48, 46, 46, 45, 44, 43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 39, 39, 40, 40, 41, 42, 43, 43, 44, 45, 45, +50, 48, 47, 46, 45, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 45, +50, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 41, 42, 42, 43, 44, 44, 45, +49, 48, 47, 45, 44, 43, 42, 41, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, 44, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42, 43, 44, 45, +48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 36, 36, 37, 38, 39, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 44, 43, 42, 40, 40, 39, 38, 37, 36, 35, 35, 34, 34, 34, 33, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 43, 42, 41, 41, 40, 39, 38, 37, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 44, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, +48, 47, 46, 45, 43, 42, 41, 40, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +48, 47, 46, 45, 44, 42, 41, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 33, 33, 33, 32, 32, 32, 32, 33, 33, 33, 34, 35, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 44, +49, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 36, 35, 34, 34, 34, 33, 33, 33, 32, 33, 33, 33, 33, 34, 34, 35, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 44, 44, +49, 48, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 37, 36, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 43, 44, 44, +49, 48, 47, 46, 44, 43, 42, 41, 40, 39, 38, 38, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 40, 41, 42, 43, 44, 44, +49, 48, 47, 46, 44, 43, 42, 41, 41, 40, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 44, +49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 38, 39, 39, 40, 41, 42, 43, 44, 44, 45, +50, 49, 47, 46, 45, 44, 43, 42, 42, 41, 40, 39, 38, 38, 38, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 39, 39, 40, 40, 41, 42, 43, 44, 45, 45, +50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 40, 39, 39, 38, 38, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 38, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 45, +50, 49, 48, 47, 46, 45, 44, 44, 43, 42, 41, 40, 39, 39, 39, 39, 37, 37, 37, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 40, 41, 41, 42, 43, 44, 44, 45, 45, +51, 50, 49, 48, 47, 46, 46, 44, 43, 42, 42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 40, 41, 41, 42, 43, 43, 44, 45, 46, 46, +52, 51, 49, 48, 47, 47, 47, 45, 44, 43, 42, 42, 41, 40, 40, 39, 39, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 41, 41, 42, 43, 43, 44, 45, 45, 46, 47, +52, 51, 50, 49, 48, 48, 48, 46, 45, 44, 43, 42, 42, 41, 41, 40, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 45, 46, 46, 47, +53, 52, 50, 49, 48, 47, 47, 46, 45, 44, 43, 43, 42, 42, 42, 41, 41, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 42, 42, 43, 43, 44, 44, 45, 46, 46, 47, 47, +53, 51, 50, 49, 48, 47, 47, 46, 46, 45, 44, 43, 43, 42, 42, 42, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 43, 43, 43, 44, 44, 45, 46, 47, 47, 47, 48, +53, 52, 52, 50, 49, 48, 48, 47, 46, 46, 45, 44, 45, 44, 43, 43, 43, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 44, 44, 44, 45, 45, 46, 47, 48, 48, 49, 48, +//B - Ch 0 +45, 44, 43, 42, 42, 41, 40, 39, 39, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 35, 34, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 39, +45, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 39, 39, +45, 44, 43, 42, 41, 40, 40, 40, 38, 38, 37, 37, 36, 36, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 39, +44, 43, 43, 42, 41, 40, 40, 40, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, 38, 38, +44, 43, 42, 41, 41, 40, 40, 39, 38, 38, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 36, 36, 37, 37, 37, 38, +44, 43, 42, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 37, 37, 38, +43, 43, 42, 41, 40, 39, 39, 38, 38, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 37, 36, +43, 42, 41, 41, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, +43, 42, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, +43, 42, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, +42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, +42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 35, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 36, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, +42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, +43, 42, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 33, 33, 33, 32, 32, 33, 32, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, +43, 42, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, +43, 42, 41, 41, 40, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 35, +43, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 35, +43, 42, 42, 41, 40, 40, 39, 38, 38, 37, 37, 37, 36, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, +44, 43, 42, 41, 40, 40, 39, 39, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 36, +44, 43, 42, 41, 41, 40, 40, 39, 39, 38, 38, 37, 36, 37, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 36, +44, 43, 43, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, +45, 44, 43, 42, 41, 41, 41, 40, 39, 39, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, +45, 44, 43, 43, 42, 42, 42, 41, 39, 39, 38, 37, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 35, 34, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 38, +46, 44, 44, 43, 42, 42, 42, 41, 40, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 38, 38, 38, 39, +46, 45, 44, 43, 42, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 39, +46, 45, 44, 43, 43, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 39, 40, 40, +48, 45, 46, 44, 44, 42, 42, 41, 41, 41, 40, 40, 39, 39, 38, 37, 37, 37, 36, 37, 37, 37, 36, 37, 36, 37, 37, 37, 37, 37, 38, 37, 38, 38, 38, 39, 40, 41, 41, 41, 40, +}; +uint32_t ref_transform = 3; +uint32_t grid_width = 31; +uint32_t grid_height = 41; diff --git a/mods/collab-edit.sh b/mods/collab-edit.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA COLLABORATION EDIT +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/rsync --rsh='/usr/bin/ssh -p 18888' -avr -P /home/pi/Videos/$1 tarina@tarina.org:/home/tarina/Videos --delete diff --git a/mods/collab-pull.sh b/mods/collab-pull.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA COLLABORATION PULL +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/rsync -e '/usr/bin/ssh -p 18888' -avr -P tarina@tarina.org:/home/tarina/Videos/$1 /home/pi/Videos diff --git a/mods/collab-push.sh b/mods/collab-push.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA COLLABORATION PUSH +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/rsync -e "/usr/bin/ssh -p 18888" -avr -P /home/pi/Videos/$1 tarina@tarina.org:/home/tarina/Videos 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/tarina-upload.sh b/mods/tarina-upload.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# TARINA.ORG MOD +# $1 filmtitle +# $2 filename +PATH=`pwd` + +/usr/bin/scp -P 18888 $2 tarina@tarina.org:/home/tarina/videos/$1.mp4 diff --git a/mods/upload-mods-enabled b/mods/upload-mods-enabled @@ -0,0 +1,5 @@ +youtube-upload +tarina-upload +collab-pull +collab-push +collab-edit diff --git a/mods/youtube-upload.sh b/mods/youtube-upload.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# YOUTUBE-UPLOAD MOD +# $1 filmtitle +# $2 filename +PATH=`pwd` + +printf "\033c" + +/bin/cat <<'EOF' + + _ _ _ + _ | | | | | | + _ _ ___ _ _| |_ _ _| | _ ____ _ _ ____ | | ___ ____ _ | | +| | | |/ _ \| | | | _) | | | || \ / _ ) | | | | _ \| |/ _ \ / _ |/ || | +| |_| | |_| | |_| | |_| |_| | |_) | (/ / | |_| | | | | | |_| ( ( | ( (_| | + \__ |\___/ \____|\___)____|____/ \____) \____| ||_/|_|\___/ \_||_|\____| +(____/ |_| + + +EOF +read -p "Youtube film title: " title +read -p "Film description: " description + + +while true; do + read -p 'Do you want your video public (y)es (n)o?:' yn + case $yn in + [Yy]* ) privacy='public' ; break ;; + [Nn]* ) privacy='private'; break;; + * ) echo "Please answer yes or no.";; + esac +done + +while true; do + echo "Do you want to upload video $title with description $description to a $privacy Youtube video?" + read -p 'Is this correct (y)es (n)o?:' yn + case $yn in + [Yy]* ) echo "OK!" ; break ;; + [Nn]* ) echo "NOPE!" ; exit;; + * ) echo "Please answer yes or no.";; + esac +done + +/usr/bin/python3 $PATH/mods/youtube-upload/youtube_upload/__main__.py --title="$title" --description="$description" --privacy="$privacy" $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,23 @@ +import sys +import pyshorteners + +def shorten_url(url): + s = pyshorteners.Shortener() + short_url = s.tinyurl.short(url) + return short_url + +def get_code(authorize_url): + sys.stderr.write("\x1b[2J\x1b[H") + short_url = shorten_url(authorize_url) + """Show authorization URL and return the code the user wrote.""" + message = "Check this link in your browser: " + short_url + sys.stderr.write("\n") + sys.stderr.write("\n") + sys.stderr.write("Youtube authentication required!\n") + 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: ") 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/srv/static/Videos b/srv/static/Videos @@ -0,0 +1 @@ +/home/pi/Videos/ +\ No newline at end of file diff --git a/srv/static/style.css b/srv/static/style.css @@ -0,0 +1,22 @@ +body +{ + margin: 0px auto; + text-align: center; + background-color:#000; + color: #f4f4f4; + font-family: monospace; +} + +pre +{ + margin: 5px auto; + padding: 3px; + display: inline-block; + background-color: #555; + color: #fff; +} + +a +{ + color: #FCD612; +} diff --git a/srv/tarinaserver.py b/srv/tarinaserver.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 + +import web +import os +import socket +import ifaddr +import sys +import time +import random +import hashlib + +# Get path of the current dir, then use it as working directory: +rundir = os.path.dirname(__file__) +if rundir != '': + os.chdir(rundir) + +filmfolder = '/home/pi/Videos/' + +basedir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(basedir) + +# Link video directory to static dir +if os.path.isfile('static/Videos') == False: + os.system("ln -s -t static/ " + filmfolder) + +films = [] + +#NETWORKS + +networks=[] +adapters = ifaddr.get_adapters() +for adapter in adapters: + print("IPs of network adapter " + adapter.nice_name) + for ip in adapter.ips: + if '::' not in ip.ip[0] and '127.0.0.1' != ip.ip: + print(ip.ip) + networks.append(ip.ip) +network=networks[0] + +urls = ( + '/?', 'index', + '/f/(.*)?', 'films' +) + +app = web.application(urls, globals()) +render = web.template.render('templates/', base="base") +web.config.debug=False +os.system('rm '+basedir+'/sessions/*') +store = web.session.DiskStore(basedir + '/sessions/') +session = web.session.Session(app,store,initializer={'login': 0, 'user': '', 'backurl': '', 'bildsida': 0, 'cameras': [], 'reload': 0, 'randhash':''}) + +port=55555 +ip='0.0.0.0' +cameras=[] + +session.randhash = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest() + +##---------------Connection---------------------------------------------- + +def pingtocamera(host, port, data): + print("Sending to "+host+" on port "+str(port)+" DATA:"+data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.01) + try: + while True: + s.connect((host, port)) + s.send(str.encode(data)) + if host not in cameras and host not in networks: + session.cameras.append(host) + print("Found camera! "+host) + print("Sent to server..") + break + except: + ('did not connect') + s.close() + +def sendtocamera(host, port, data): + print("Sending to "+host+" on port "+str(port)+" DATA:"+data) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.1) + try: + while True: + s.connect((host, port)) + s.send(str.encode(data)) + print("Sent to server..") + break + except: + ('did not connect') + s.close() + + +def getfilms(filmfolder): + #get a list of films, in order of settings.p file last modified + films_sorted = [] + films = next(os.walk(filmfolder))[1] + for i in films: + if os.path.isfile(filmfolder + i + '/settings.p') == True: + lastupdate = os.path.getmtime(filmfolder + i + '/' + 'settings.p') + films_sorted.append((i,lastupdate)) + else: + films_sorted.append((i,0)) + films_sorted = sorted(films_sorted, key=lambda tup: tup[1], reverse=True) + print(films_sorted) + return films_sorted + +#------------Count scenes-------- + +def countscenes(filmfolder, filmname): + scenes = 0 + try: + allfiles = os.listdir(filmfolder + filmname) + except: + allfiles = [] + scenes = 0 + for a in allfiles: + if 'scene' in a: + scenes = scenes + 1 + return scenes + +#------------Count shots-------- + +def countshots(filmname, filmfolder, scene): + shots = 0 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scene).zfill(3)) + except: + allfiles = [] + shots = 0 + for a in allfiles: + if 'shot' in a: + shots = shots + 1 + return shots + +#------------Count takes-------- + +def counttakes(filmname, filmfolder, scene, shot): + takes = 0 + try: + allfiles = os.listdir(filmfolder + filmname + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3)) + except: + allfiles = [] + return takes + for a in allfiles: + if '.mp4' in a or '.h264' in a: + takes = takes + 1 + return takes + +class index: + def GET(self): + films = getfilms(filmfolder) + renderedfilms = [] + unrenderedfilms = [] + for f in films: + if os.path.isfile('static/Videos/' + f[0] + '/' + f[0] + '.mp4') == True: + renderedfilms.append(f[0]) + else: + unrenderedfilms.append(f[0]) + i=web.input(func=None,selected=None) + if i.selected != None: + sendtocamera(ip,port,'SELECTED:'+i.selected) + if i.func == 'search': + session.cameras=[] + # ping ip every 10 sec while not recording to connect cameras + pingip=0 + while pingip < 255 : + pingip+=1 + pingtocamera(network[:-3]+str(pingip),port,'PING') + elif i.func == 'record': + sendtocamera(ip,port,'REC') + elif i.func == 'retake': + sendtocamera(ip,port,'RETAKE') + elif i.func == 'up': + sendtocamera(ip,port,'UP') + elif i.func == 'down': + sendtocamera(ip,port,'DOWN') + elif i.func == 'left': + sendtocamera(ip,port,'LEFT') + elif i.func == 'right': + sendtocamera(ip,port,'RIGHT') + elif i.func == 'view': + sendtocamera(ip,port,'VIEW') + elif i.func == 'middle': + sendtocamera(ip,port,'MIDDLE') + elif i.func == 'delete': + sendtocamera(ip,port,'DELETE') + elif i.func == 'picture': + sendtocamera(ip,port,'PICTURE') + session.randhash = hashlib.md5(str(random.getrandbits(256)).encode('utf-8')).hexdigest() + session.reload = 1 + if i.func != None: + session.reload = 1 + raise web.seeother('/') + time.sleep(1) + interface=open('/dev/shm/interface','r') + vumeter=open('/dev/shm/vumeter','r') + menu=interface.readlines() + vumetermessage=vumeter.readlines()[0].rstrip('\n') + try: + selected=int(menu[0]) + except: + selected=0 + try: + name=menu[3].split(':')[1] + name=name.rstrip('\n') + except: + name='' + try: + scene=menu[4].split(':')[1].split('/')[0] + except: + scene=1 + try: + shot=menu[5].split(':')[1].split('/')[0] + except: + shot=1 + try: + take=menu[6].split(':')[1].split('/')[0] + except: + take=1 + session.reload = 0 + thumb="/static/Videos/"+name+"/scene"+str(scene).zfill(3)+"/shot"+str(shot).zfill(3)+"/picture"+str(take).zfill(3)+".jpeg" + print(thumb) + if os.path.isfile(basedir+thumb) == False: + print(basedir+thumb) + thumb='' + return render.index(renderedfilms, unrenderedfilms, session.cameras, menu, selected,name,scene,shot,take,str,session.randhash,thumb,vumetermessage,i.func) + +class films: + def GET(self, film): + shots = 0 + takes = 0 + i = web.input(page=None, scene=None, shot=None, take=None) + if i.scene != None: + shots = countshots(film, filmfolder, i.scene) + takes = counttakes(film, filmfolder, i.scene, i.shot) + if i.scene != None and i.shot != None: + shots = countshots(film, filmfolder, i.scene) + scenes = countscenes(filmfolder, film) + return render.filmpage(film, scenes, str, filmfolder, counttakes, countshots, shots, i.scene, takes, i.shot, i.take) + +application = app.wsgifunc() + diff --git a/srv/tarinaserver.pyc b/srv/tarinaserver.pyc Binary files differ. diff --git a/srv/templates/base.html b/srv/templates/base.html @@ -0,0 +1,12 @@ +$def with (content) +<!doctype html> +<HEAD> + <meta charset="utf-8"> + <title>Tarina | video & audio recorder with glue</title> + <link rel="stylesheet" href="/static/style.css?v=33" type="text/css" rel="stylesheet"/> +</HEAD> +<BODY> + + $:content + +</BODY> diff --git a/srv/templates/filmpage.html b/srv/templates/filmpage.html @@ -0,0 +1,39 @@ +$def with (film, scenes, str, filmfolder, counttakes, countshots, shots, scene, takes, shot, take) +$ video = '' +$if take != None: + <h1>$film | scene $scene | shot $shot | take $take</h1> + <h2><a href='?scene=$scene&shot=$shot'>go back</a></h2><br> + $ video = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(take).zfill(3) + '.mp4' +$elif shot != None: + <h1>$film | scene $scene | shot $shot</h1> + <h2><a href='?scene=$scene'>go back</a></h2><br> + $ video = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(takes).zfill(3) + '.mp4' +$elif scene != None: + <h1>$film | scene $scene</h1> + <h2><a href='/f/$film'>go back</a><h2><br> + $ video = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/scene.mp4' +$elif scene == None: + <h1>$film</h1> + $ video = '/static/Videos/' + film + '/' + film + '.mp4' +<video width="80%" controls> +<source src="$video" type="video/mp4"> +Your brower is caput +</video> +<p>Copy project to your destination:</p> +<pre>scp -r pi@tarina.local:~/Videos/$film ~/films/$film </pre> + +$if shot != None: + $for t in range(takes): + $ thumbnail_url = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(shot).zfill(3) + '/take' + str(t+1).zfill(3) + '.jpeg' + <a href="?scene=$scene&shot=$shot&take=${str(t+1)}"><img width="80%" src="$thumbnail_url"/></a><br> +$elif scene != None: + $for s in range(shots) + $ t = countshots(film, filmfolder, scene) + $ p = counttakes(film, filmfolder, scene, s+1) + $ thumbnail_url = '/static/Videos/' + film + '/scene' + str(scene).zfill(3) + '/shot' + str(s+1).zfill(3) + '/take' + str(p).zfill(3) + '.jpeg' + <a href="?scene=$scene&shot=${str(s+1)}"><img width="80%" src="$thumbnail_url"/></a><br> +$else: + $for s in range(scenes): + $ t = counttakes(film, filmfolder, s+1, 1) + $ thumbnail_url = '/static/Videos/' + film + '/scene' + str(s+1).zfill(3) + '/shot001/take' + str(t).zfill(3) + '.jpeg' + <a href="?scene=${str(s+1)}"><img width="80%" src="$thumbnail_url"/></a><br> diff --git a/srv/templates/index.html b/srv/templates/index.html @@ -0,0 +1,61 @@ +$def with (renderedfilms, unrenderedfilms, cameras, menu, selected,name,scene,shot,take,str,randhash,thumb,vumetermessage,func) +$var renderedfilms = renderedfilms +$var unrenderedfilms = unrenderedfilms +<script> +function timedRefresh(timeoutPeriod) { + setTimeout("location.reload(true);",timeoutPeriod); +} +</script> +connected +$for i in cameras: + $i +<br> +<a href="/?func=view">VIEW</a> <a href="/?func=up">__UP__</a> <a href="/?func=record">RECORD</a><br> +<a href="/?func=left">LEFT</a> <a href="/?func=middle">MIDDLE</a> <a href="/?func=right">RIGHT</a><br> +<a href="/?func=delete">DELETE</a> <a href="/?func=down">DOWN</a> <a href="/?func=retake">RETAKE</a><br> +<a href="/?func=picture">PICTURE</a> +<a href="/?func=search">SEARCH</a> +<div id="menu" style="margin:0 auto; width:99%"> +$vumetermessage +<br> +$ y=0 +$for m in menu[3:]: + $if selected == y: + <b>$m[:-1]</b> + $else: + <a href="?selected=$y">$m[:-1]</a> + $ y+=1 +<br> +</div> +$if thumb != '': + $ picture="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/shot" + str(shot).zfill(3) + "/picture" + str(take).zfill(3) + ".jpeg" +$else: + $ picture="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/shot" + str(shot).zfill(3) + "/take" + str(take).zfill(3) + ".jpeg" + + +$ take_link="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/shot" + str(shot).zfill(3) + "/take" + str(take).zfill(3) + ".mp4" +$ scene_link="static/Videos/" + name + "/scene" + str(scene).zfill(3) + "/scene.mp4" +$ film_link="static/Videos/" + name + "/" +name+ ".mp4" +$if selected == 0: + <a href='$film_link'><img width="99%" src="$picture?$randhash"/></a><br> +$elif selected == 1: + <a href='$scene_link'><img width="99%" src="$picture?$randhash"/></a><br> +$elif selected > 1: + <a href='$take_link'><img width="99%" src="$picture?$randhash"/></a><br> +<br> +$if func=='show_all_films': + <h1>FILMS</h1> + + $for i in renderedfilms: + <p>--------------------------------------------------------------</p> + <h2>$i</h2> + <a href="static/Videos/$i/${i}.mp4"><img width="80%" src="static/Videos/$i/scene001/shot001/take001.jpeg?$randhash"/></a><br> + <p>Copy project to your destination:</p> + <pre>scp -r pi@tarina.local:~/Videos/$i ~/films/$i</pre> + <h1>Films unrendered</h1> + + $for i in unrenderedfilms: + <h2>$i </h2> + <p>Copy project to your destination:</p> + <pre>scp -r pi@tarina.local:~/Videos/$i ~/films/$i</pre> + diff --git a/startinterface.sh b/startinterface.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo "Have fun!" > /dev/shm/vumeter +echo "For the lulz" > /dev/shm/interface +cd ./gui +./tarinagui.bin