Compare commits
6 Commits
3946bc53c8
...
0b47bda1aa
Author | SHA1 | Date |
---|---|---|
southerntofu | 0b47bda1aa | |
southerntofu | ee6b30b0c0 | |
southerntofu | 8e473b7948 | |
southerntofu | d51a5089f5 | |
southerntofu | 03ddeb2608 | |
southerntofu | ba55cfd472 |
|
@ -1 +1,2 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.*.sw*
|
||||||
|
|
28
README.md
28
README.md
|
@ -1,4 +1,4 @@
|
||||||
### SimpleerTube
|
# SimpleerTube
|
||||||
|
|
||||||
Active Known Instances:
|
Active Known Instances:
|
||||||
- https://simpleertube.metalune.xyz
|
- https://simpleertube.metalune.xyz
|
||||||
|
@ -11,3 +11,29 @@ If you want to visit any page from your PeerTube instance of choice in SimpleerT
|
||||||
So, `https://videos.lukesmith.xyz/accounts/luke` becomes `https://simpleertube.metalune.xyz/videos.lukesmith.xyz/accounts/luke`.
|
So, `https://videos.lukesmith.xyz/accounts/luke` becomes `https://simpleertube.metalune.xyz/videos.lukesmith.xyz/accounts/luke`.
|
||||||
|
|
||||||
If you visit the main page, you can search globally (it uses [Sepia Search](https://sepiasearch.org) in the backend).
|
If you visit the main page, you can search globally (it uses [Sepia Search](https://sepiasearch.org) in the backend).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
You need to setup a few dependencies first, usually using pip (`sudo apt install python3-pip` on Debian):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo pip3 install quart bs4 html2text lxml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If there are other dependencies that are not packaged with your system, please report them to us so they can be added to this README.
|
||||||
|
|
||||||
|
Now you can run a development environment like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python3 main.py # Starts on localhost:5000
|
||||||
|
$ python3 main.py 192.168.42.2 # Starts on 192.168.42.2:5000
|
||||||
|
$ python3 main.py 7171 # Starts on localhost:7171
|
||||||
|
$ python3 main.py 192.168.42.2 7171 # Starts on 192.168.42.2:7171
|
||||||
|
$ python3 main.py ::1 7171 # Also works with IPv6 addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
It is strongly disrecommended to run the production using this command. Instead, please refer to the [Quart deployment docs](https://pgjones.gitlab.io/quart/tutorials/deployment.html).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This software is distributed under the AGPLv3 license. You can find a copy in the [LICENSE](LICENSE) file.
|
||||||
|
|
40
main.py
40
main.py
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||||
from math import ceil
|
from math import ceil
|
||||||
import peertube
|
import peertube
|
||||||
import html2text
|
import html2text
|
||||||
|
import sys
|
||||||
|
|
||||||
h2t = html2text.HTML2Text()
|
h2t = html2text.HTML2Text()
|
||||||
h2t.ignore_links = True
|
h2t.ignore_links = True
|
||||||
|
@ -11,6 +12,7 @@ h2t.ignore_links = True
|
||||||
class VideoWrapper:
|
class VideoWrapper:
|
||||||
def __init__(self, a, quality):
|
def __init__(self, a, quality):
|
||||||
self.name = a["name"]
|
self.name = a["name"]
|
||||||
|
self.uuid = a["uuid"]
|
||||||
self.channel = a["channel"]
|
self.channel = a["channel"]
|
||||||
self.description = a["description"]
|
self.description = a["description"]
|
||||||
self.thumbnailPath = a["thumbnailPath"]
|
self.thumbnailPath = a["thumbnailPath"]
|
||||||
|
@ -18,6 +20,7 @@ class VideoWrapper:
|
||||||
self.category = a["category"]
|
self.category = a["category"]
|
||||||
self.licence = a["licence"]
|
self.licence = a["licence"]
|
||||||
self.language = a["language"]
|
self.language = a["language"]
|
||||||
|
self.captions = a["captions"]
|
||||||
self.privacy = a["privacy"]
|
self.privacy = a["privacy"]
|
||||||
self.tags = a["tags"]
|
self.tags = a["tags"]
|
||||||
|
|
||||||
|
@ -128,6 +131,11 @@ async def simpleer_search_redirect():
|
||||||
query = (await request.form)["query"]
|
query = (await request.form)["query"]
|
||||||
return redirect("/search/" + query)
|
return redirect("/search/" + query)
|
||||||
|
|
||||||
|
@app.route("/search", methods = ["GET"])
|
||||||
|
async def simpleer_search_get_redirect():
|
||||||
|
query = request.args.get("query")
|
||||||
|
return redirect("/search/" + query)
|
||||||
|
|
||||||
@app.route("/search/<string:query>", defaults = {"page": 1})
|
@app.route("/search/<string:query>", defaults = {"page": 1})
|
||||||
@app.route("/search/<string:query>/<int:page>")
|
@app.route("/search/<string:query>/<int:page>")
|
||||||
async def simpleer_search(query, page):
|
async def simpleer_search(query, page):
|
||||||
|
@ -148,6 +156,13 @@ async def simpleer_search(query, page):
|
||||||
|
|
||||||
@app.route("/<string:domain>/")
|
@app.route("/<string:domain>/")
|
||||||
async def instance(domain):
|
async def instance(domain):
|
||||||
|
# favicon.ico is not a domain name
|
||||||
|
if domain == "favicon.ico":
|
||||||
|
return await render_template(
|
||||||
|
"error.html",
|
||||||
|
error_number = "404",
|
||||||
|
error_reason = "We don't have a favicon yet. If you would like to contribute one, please send it to ~metalune/public-inbox@lists.sr.ht"
|
||||||
|
), 404
|
||||||
return redirect("/" + domain + "/videos/trending")
|
return redirect("/" + domain + "/videos/trending")
|
||||||
|
|
||||||
@app.route("/<string:domain>/videos/local", defaults = {"page": 1})
|
@app.route("/<string:domain>/videos/local", defaults = {"page": 1})
|
||||||
|
@ -250,6 +265,7 @@ async def search(domain, term, page):
|
||||||
@app.route("/<string:domain>/videos/watch/<string:id>/")
|
@app.route("/<string:domain>/videos/watch/<string:id>/")
|
||||||
async def video(domain, id):
|
async def video(domain, id):
|
||||||
data = peertube.video(domain, id)
|
data = peertube.video(domain, id)
|
||||||
|
data["captions"] = peertube.video_captions(domain, id)
|
||||||
quality = request.args.get("quality")
|
quality = request.args.get("quality")
|
||||||
embed = request.args.get("embed")
|
embed = request.args.get("embed")
|
||||||
vid = VideoWrapper(data, quality)
|
vid = VideoWrapper(data, quality)
|
||||||
|
@ -281,7 +297,6 @@ async def video(domain, id):
|
||||||
embed=embed,
|
embed=embed,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_channel_or_account_name(domain, name):
|
def build_channel_or_account_name(domain, name):
|
||||||
if '@' in name:
|
if '@' in name:
|
||||||
return name
|
return name
|
||||||
|
@ -396,5 +411,26 @@ async def video_channels__about(domain, name):
|
||||||
about = peertube.video_channel(domain, name)
|
about = peertube.video_channel(domain, name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Subtitles/captions proxying ---
|
||||||
|
@app.route("/<string:domain>/videos/watch/<string:id>/<string:lang>.vtt")
|
||||||
|
async def subtitles(domain, id, lang):
|
||||||
|
try:
|
||||||
|
return peertube.video_captions_download(domain, id, lang)
|
||||||
|
except Exception as e:
|
||||||
|
return await render_template(
|
||||||
|
"error.html",
|
||||||
|
error_number = "500",
|
||||||
|
error_reason = e
|
||||||
|
), 500
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run()
|
if len(sys.argv) == 3:
|
||||||
|
interface = sys.argv[1]
|
||||||
|
port = sys.argv[2]
|
||||||
|
elif len(sys.argv) == 2:
|
||||||
|
interface = "127.0.0.1"
|
||||||
|
port = sys.argv[1]
|
||||||
|
else:
|
||||||
|
interface = "127.0.0.1"
|
||||||
|
port = "5000"
|
||||||
|
app.run(host=interface, port=port)
|
||||||
|
|
10
peertube.py
10
peertube.py
|
@ -22,6 +22,16 @@ def video(domain, id):
|
||||||
url = "https://" + domain + "/api/v1/videos/" + id
|
url = "https://" + domain + "/api/v1/videos/" + id
|
||||||
return json.loads(requests.get(url).text)
|
return json.loads(requests.get(url).text)
|
||||||
|
|
||||||
|
def video_captions(domain, id):
|
||||||
|
url = "https://" + domain + "/api/v1/videos/" + id + "/captions"
|
||||||
|
return json.loads(requests.get(url).text)
|
||||||
|
|
||||||
|
def video_captions_download(domain, id, lang):
|
||||||
|
# URL is hardcoded to prevent further proxying. URL may change with updates, see captions API
|
||||||
|
# eg. https://kolektiva.media/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180a6d/captions
|
||||||
|
url = "https://" + domain + "/lazy-static/video-captions/" + id + '-' + lang + ".vtt"
|
||||||
|
return requests.get(url).text
|
||||||
|
|
||||||
def search(domain, term, start=0, count=10):
|
def search(domain, term, start=0, count=10):
|
||||||
url = "https://" + domain + "/api/v1/search/videos?start=" + str(start) + "&count=" + str(count) + "&search=" + term + "&sort=-match&searchTarget=local"
|
url = "https://" + domain + "/api/v1/search/videos?start=" + str(start) + "&count=" + str(count) + "&search=" + term + "&sort=-match&searchTarget=local"
|
||||||
return json.loads(requests.get(url).text)
|
return json.loads(requests.get(url).text)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}ERROR: {% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Error {{ error_number }}</h1>
|
||||||
|
<p>{{ error_reason }}</p>
|
||||||
|
{% endblock %}
|
|
@ -22,7 +22,8 @@ By:
|
||||||
<b>Resolutions:</b>
|
<b>Resolutions:</b>
|
||||||
{% else %}
|
{% else %}
|
||||||
<video height="300" style="max-width: 100%" controls>
|
<video height="300" style="max-width: 100%" controls>
|
||||||
<source src="{{ video.video }}">
|
<source src="{{ video.video }}">{% for track in video.captions.data %}
|
||||||
|
<track kind="subtitles" srclang="{{ track.language.id }}" label="{{ track.language.label }}" src="/{{ domain }}/videos/watch/{{ video.uuid }}/{{ track.language.id }}.vtt">{% endfor %}
|
||||||
</video>
|
</video>
|
||||||
<br>
|
<br>
|
||||||
<b>Resolutions:</b>
|
<b>Resolutions:</b>
|
||||||
|
|
Loading…
Reference in New Issue