├── 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://www.youtube.com/watch?v=3BYNj6Yvl8I "Video Title")
21 |
22 | ```
23 |
24 | Here's how it'd look:
25 |
26 | [](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 | [](http://www.youtube.com/watch?v=3BYNj6Yvl8I "Video Title")
35 |
36 |
37 | ```
38 | [](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 |
22 |
23 |
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 |
45 |
46 |
47 | Examples:
48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
61 |
62 |
63 |
64 |
72 |
73 |
198 |
199 |
206 |
207 |
--------------------------------------------------------------------------------