├── src ├── static │ ├── after.png │ ├── arrow.png │ ├── clock.png │ ├── fontm.ttf │ ├── name.png │ ├── play.png │ ├── before.jpg │ ├── shadow.png │ └── index.css ├── server.py ├── main.py ├── processor.py └── templates │ └── index.html ├── wsgi.py ├── TODO ├── default.nix ├── LICENSE ├── .gitignore └── README.md /src/static/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/after.png -------------------------------------------------------------------------------- /src/static/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/arrow.png -------------------------------------------------------------------------------- /src/static/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/clock.png -------------------------------------------------------------------------------- /src/static/fontm.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/fontm.ttf -------------------------------------------------------------------------------- /src/static/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/name.png -------------------------------------------------------------------------------- /src/static/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/play.png -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from src.server import app 2 | 3 | if __name__ == "__main__": 4 | app.run() 5 | -------------------------------------------------------------------------------- /src/static/before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/before.jpg -------------------------------------------------------------------------------- /src/static/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhasbini/yt-embed/HEAD/src/static/shadow.png -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | FLASK_APP=src/server.py flask run 4 | 5 | pipenv run gunicorn -w 4 -b 0.0.0.0:8080 wsgi:app 6 | 7 | add Arabic language support (e.g. https://www.youtube.com/watch?v=GbCnfeCvf3U) 8 | 9 | Configure sizes (which size to use) 10 | Add dropdown to the index form 11 | 12 | - debug https://yt-embed.herokuapp.com/embed?v=E2V817iAL8A 13 | - automatically extract thumbnail -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, send_file, render_template 2 | from io import BytesIO 3 | 4 | from .processor import embed_image 5 | 6 | app = Flask(__name__) 7 | 8 | 9 | @app.route("/") 10 | def hello(): 11 | return render_template("index.html") 12 | 13 | 14 | @app.route("/embed") 15 | def embed(): 16 | video_id = request.args.get("v", None) 17 | img = request.args.get("img", "sddefault") 18 | 19 | return send_file(BytesIO(embed_image(video_id, img)), mimetype="image/png") 20 | 21 | 22 | if __name__ == "__main__": 23 | app.config['TEMPLATES_AUTO_RELOAD'] = True 24 | app.run() 25 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = import (builtins.fetchGit { 3 | name = "nixpkgs"; 4 | url = "https://github.com/NixOS/nixpkgs-channels"; 5 | rev = "4762fba469e2baa82f983b262e2c06ac2fdaae67"; 6 | }) {}; 7 | 8 | py3 = nixpkgs.python37.override { 9 | packageOverrides = self: super: with self; { 10 | }; 11 | }; 12 | 13 | install_packages = [ 14 | (py3.buildEnv.override { 15 | ignoreCollisions = true; 16 | extraLibs = with py3.pkgs; [ 17 | flask 18 | pillow 19 | gunicorn 20 | ipdb 21 | requests 22 | ]; 23 | }) 24 | ]; 25 | 26 | 27 | in 28 | nixpkgs.stdenv.mkDerivation { 29 | name = "yt-embed"; 30 | buildInputs = install_packages; 31 | } 32 | -------------------------------------------------------------------------------- /src/static/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Inter', sans-serif; 3 | background-color: #eee; 4 | color: #000; 5 | } 6 | 7 | #container { 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | 12 | padding-bottom: 6em; 13 | } 14 | 15 | img { 16 | max-width: 600px; 17 | filter: drop-shadow(5px 5px 10px #4444dd); 18 | } 19 | 20 | .icon { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | a, 27 | a:focus, 28 | a:visited { 29 | color: inherit; 30 | } 31 | 32 | #arrow-down { 33 | display: none; 34 | } 35 | 36 | #footer { 37 | clear: both; 38 | position: relative; 39 | height: 5em; 40 | margin-top: -5em; 41 | text-align: center; 42 | } 43 | 44 | @media screen and (max-width: 1300px) { 45 | #container { 46 | flex-direction: column; 47 | } 48 | 49 | #arrow-down { 50 | display: block; 51 | } 52 | 53 | #arrow-left { 54 | display: none; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 M. Hasbini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PIL import Image, ImageDraw, ImageFont 3 | import requests 4 | 5 | # data = {"author_url":"https:\/\/www.youtube.com\/channel\/UCZE6iNWl5dDd8hj88wAAmYw","title":"SPA server MVP","version":"1.0","width":459,"provider_url":"https:\/\/www.youtube.com\/","author_name":"M Hasbini","provider_name":"YouTube","height":344,"thumbnail_height":360,"thumbnail_url":"https:\/\/i.ytimg.com\/vi\/ChlK4vq8Nwk\/hqdefault.jpg","type":"video","thumbnail_width":480,"html":"\u003ciframe width=\"459\" height=\"344\" src=\"https:\/\/www.youtube.com\/embed\/ChlK4vq8Nwk?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c\/iframe\u003e"} 6 | 7 | # https://i.ytimg.com/vi/ChlK4vq8Nwk/maxresdefault.jpg 8 | # https://i.ytimg.com/vi/ChlK4vq8Nwk/hqdefault.jpg 9 | 10 | # import ipdb; ipdb.set_trace(); 11 | 12 | # print("test") 13 | 14 | filename = "maxresdefault.jpg" 15 | 16 | fnt = ImageFont.truetype("fontm.ttf", 14) 17 | fnt_b = ImageFont.truetype("fontm.ttf", 18) 18 | img = Image.open(filename) 19 | 20 | play = Image.open("play.png") 21 | 22 | w, h = img.size 23 | 24 | draw = ImageDraw.Draw(img) 25 | 26 | img.paste(play, (int((w - 68) / 2), int((h - 48) / 2)), play) 27 | 28 | shadow = Image.open("shadow.png").resize((w, h * 2)) 29 | 30 | img.paste(shadow, (0, 0), shadow) 31 | 32 | clock = Image.open("clock.png") 33 | img.paste(clock, (w - 160, 10), clock) 34 | s = "Watch later" 35 | draw.text( 36 | (w - 160 - int(len(s) / 2) - 15, 10 + 36 + 5), s, font=fnt, fill=(255, 255, 255) 37 | ) 38 | 39 | arrow = Image.open("arrow.png") 40 | img.paste(arrow, (w - 70, 10), arrow) 41 | s = "Share" 42 | draw.text( 43 | (w - 70 - int(len(s) / 2) + 2, 10 + 36 + 5), s, font=fnt, fill=(255, 255, 255) 44 | ) 45 | 46 | 47 | name = Image.open("name.png") 48 | img.paste(name, (20, 10 + 15), name) 49 | s = "SPA server MVP" 50 | draw.text((75, 10 + int((36 + 5) / 2)), s, font=fnt_b, fill=(255, 255, 255)) 51 | 52 | img.show() 53 | 54 | # img.save('out.png') 55 | 56 | # os.system('open out.png') 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | .DS_Store 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | -------------------------------------------------------------------------------- /src/processor.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PIL import Image, ImageDraw, ImageFont 3 | import requests 4 | from io import BytesIO 5 | 6 | 7 | def embed_image(video_id, img): 8 | image = get_image(video_id, img) 9 | 10 | w, h = image.size 11 | title = elipsis(get_title(video_id), 25) 12 | 13 | insert_play_button(image, w, h) 14 | 15 | insert_header(image, w, h, title) 16 | 17 | byte_arr = BytesIO() 18 | image.save(byte_arr, format="PNG") 19 | byte_arr = byte_arr.getvalue() 20 | 21 | return byte_arr 22 | 23 | 24 | def elipsis(_str, char_stop): 25 | return _str[:char_stop] + (_str[char_stop:] and '...') 26 | 27 | def get_title(video_id): 28 | response = requests.get( 29 | f"https://www.youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v={video_id}" 30 | ) 31 | 32 | return response.json()["title"] 33 | 34 | 35 | def get_image(video_id, img): 36 | url = f"https://img.youtube.com/vi/{video_id}/{img}.jpg" 37 | response = requests.get(url) 38 | 39 | print("url", url) 40 | 41 | return Image.open(BytesIO(response.content)) 42 | 43 | 44 | def insert_play_button(image, w, h): 45 | play = Image.open("./src/static/play.png") 46 | 47 | image.paste(play, ((w - 68) // 2, (h - 48) // 2), play) 48 | 49 | 50 | def insert_header(image, w, h, title): 51 | insert_shadow(image, w, h) 52 | insert_watch_later(image, w, h) 53 | insert_share(image, w, h) 54 | insert_title(image, w, h, title) 55 | 56 | 57 | def insert_shadow(image, w, h): 58 | shadow = Image.open("./src/static/shadow.png").resize((w, h * 2)) 59 | 60 | image.paste(shadow, (0, 0), shadow) 61 | 62 | 63 | def insert_watch_later(image, w, h): 64 | clock = Image.open("./src/static/clock.png") 65 | image.paste(clock, (w - 160, 10), clock) 66 | text = "Watch later" 67 | ImageDraw.Draw(image).text( 68 | (w - 160 - len(text) // 2 - 15, 10 + 36 + 5), 69 | text, 70 | font=ImageFont.truetype("./src/static/fontm.ttf", 14), 71 | fill=(255, 255, 255), 72 | ) 73 | 74 | 75 | def insert_share(image, w, h): 76 | arrow = Image.open("./src/static/arrow.png") 77 | image.paste(arrow, (w - 70, 10), arrow) 78 | text = "Share" 79 | ImageDraw.Draw(image).text( 80 | (w - 70 - len(text) // 2 + 2, 10 + 36 + 5), 81 | text, 82 | font=ImageFont.truetype("./src/static/fontm.ttf", 14), 83 | fill=(255, 255, 255), 84 | ) 85 | 86 | 87 | # make sure to render arabic font as well 88 | def insert_title(image, w, h, title): 89 | name = Image.open("./src/static/name.png") 90 | image.paste(name, (20, 10 + 15), name) 91 | ImageDraw.Draw(image).text( 92 | (75, 10 + (36 + 5) // 2), 93 | title, 94 | font=ImageFont.truetype("./src/static/fontm.ttf", 18), 95 | fill=(255, 255, 255), 96 | ) 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube embed image overlay 2 | 3 | A tool to add more visual cue when embedding YouTube videos in GitHub. 4 | 5 | ## Run app localy 6 | 7 | - Make sure to have [nix package manager](https://nixos.org/download.html) installed. 8 | - Clone repo & cd into it. 9 | - Run: `nix-shell` 10 | - Inside the shell run `gunicorn -w 4 -b 0.0.0.0:8080 wsgi:app` and visit http://localhost:8080/ 11 | 12 | ## Example 13 | 14 | The method I used before to embed YouTube videos inside my repos was from [here](https://stackoverflow.com/questions/11804820/how-can-i-embed-a-youtube-video-on-github-wiki-pages) 15 | 16 | 17 | For example, If I want to embed [this video](https://www.youtube.com/watch?v=3BYNj6Yvl8I) I'd use: 18 | 19 | ``` 20 | [![](http://img.youtube.com/vi/3BYNj6Yvl8I/0.jpg)](http://www.youtube.com/watch?v=3BYNj6Yvl8I "Video Title") 21 | 22 | ``` 23 | 24 | Here's how it'd look: 25 | 26 | [![](http://img.youtube.com/vi/3BYNj6Yvl8I/0.jpg)](http://www.youtube.com/watch?v=3BYNj6Yvl8I "Video Title") 27 | 28 | The answer above suggest that we take screen shot and embed it to make it easir to reason that the above is a video and not an image. 29 | 30 | This tool will automate this process and add visial cues similar to an embeded youtube video. 31 | 32 | Here's how it'd look: 33 | 34 | [![](https://yt-embed.live/embed?v=3BYNj6Yvl8I)](http://www.youtube.com/watch?v=3BYNj6Yvl8I "Video Title") 35 | 36 | 37 | ``` 38 | [![](https://yt-embed.live/embed?v=3BYNj6Yvl8I)](http://www.youtube.com/watch?v=3BYNj6Yvl8I "Video Title") 39 | ``` 40 | 41 | ## Deployment & Hosting 42 | 43 | ### systemd service 44 | 45 | ``` 46 | # cat /lib/systemd/system/yt-embed.service 47 | [Unit] 48 | Description=yt-embed 49 | 50 | [Service] 51 | Type=simple 52 | Restart=always 53 | RestartSec=5s 54 | WorkingDirectory=/home/ubuntu/yt-embed 55 | ExecStart=/home/ubuntu/.nix-profile/bin/nix-shell -I /home/ubuntu/.nix-defexpr/channels --run "gunicorn -w 4 -b 0.0.0.0:9070 wsgi:app" 56 | 57 | [Install] 58 | WantedBy=multi-user.target 59 | ``` 60 | 61 | ### nginx config 62 | 63 | ``` 64 | $ cat /etc/nginx/sites-enabled/yt-embed 65 | server { 66 | server_name yt-embed.live www.yt-embed.live; 67 | 68 | location / { 69 | proxy_pass http://localhost:9070; 70 | } 71 | 72 | 73 | listen 443 ssl; # managed by Certbot 74 | ssl_certificate /etc/letsencrypt/live/yt-embed.live/fullchain.pem; # managed by Certbot 75 | ssl_certificate_key /etc/letsencrypt/live/yt-embed.live/privkey.pem; # managed by Certbot 76 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 77 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 78 | 79 | 80 | } 81 | 82 | server { 83 | if ($host = www.yt-embed.live) { 84 | return 301 https://$host$request_uri; 85 | } # managed by Certbot 86 | 87 | 88 | if ($host = yt-embed.live) { 89 | return 301 https://$host$request_uri; 90 | } # managed by Certbot 91 | 92 | 93 | server_name yt-embed.live www.yt-embed.live; 94 | listen 80; 95 | return 404; # managed by Certbot 96 | 97 | 98 | 99 | 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | YouTube embed image overlay 5 | 6 | 7 | 8 | 9 | 10 |
11 |

YouTube embed image overlay

12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
32 | 33 |
34 |

35 | Submit video id (e.g. https://www.youtube.com/watch?v=3BYNj6Yvl8I) in the form bellow. 36 |
37 | Or append /embed?v=[video_id] directly. 38 |

39 | 40 |
41 | 42 | 43 |
44 |
45 | 46 |

47 | Examples: 48 | 49 |

53 |

54 |
55 | 56 | 57 |
58 | 59 | 61 | 62 | 63 | 64 | 72 | 73 | 198 | 199 | 206 | 207 | --------------------------------------------------------------------------------