Skip to content

Commit 26ec506

Browse files
committed
Directory structure changed
1 parent c4d591c commit 26ec506

35 files changed

+1028
-1041
lines changed

skaermedia/gapi/__init__.py renamed to apps/__init__.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
#!/usr/bin/env python3
21
#
3-
# Skaer media server
2+
# skaer media streamer
43
# Copyright (c) 2019 Emil Penchev
54
#
65
# Project page:
7-
# http://skaermedia.org
6+
# http://skaer.org
87
#
98
# licensed under GNU GPL version 3 (or later)
109
#
@@ -23,5 +22,30 @@
2322
#
2423

2524

26-
__all__ = ['login']
25+
from .sources import *
26+
27+
__all__ = ['all', 'load']
28+
29+
all_apps = None
30+
31+
32+
def load():
33+
"""
34+
Get the class for every app from the globals dictionary.
35+
Load all apps and create an instance for every app.
36+
37+
38+
"""
39+
global all_apps
40+
klasses = [klass for name, klass in globals().items() if name.endswith('App')]
41+
all_apps = [cls() for cls in klasses]
42+
43+
44+
45+
def all():
46+
"""
47+
Return a list containing every app instance.
48+
"""
49+
global all_apps
50+
return all_apps
2751

apps/sources.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .youtube import YouTubeApp
2+
from .yesmovies import YesMoviesApp
3+

apps/yesmovies.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import requests
2+
3+
4+
class YesMoviesApp(object):
5+
def __init__(self):
6+
self.info = {
7+
'name' : 'YesMovies',
8+
'path' : 'yesmovies',
9+
'description' : 'YesMovies skaer app',
10+
'cover_image' : 'http://localhost:8080/res/images/yes-movies.jpg',
11+
'category' : 'Video'
12+
}
13+
14+
def entries(self):
15+
""" Return all entries (videos) """
16+
return []
17+
18+
def register_api_routes(self, router):
19+
router.connect('GET', 'yesmovies/info', self.get_info)
20+
21+
File renamed without changes.

apps/youtube.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import os
2+
import json
3+
import youtube_dl
4+
import cherrypy
5+
import requests
6+
7+
import server_config
8+
from apps.youtube_objects import YoutubeObjects, YoutubeObjectError
9+
from apps.youtube_api import YoutubeDataApi
10+
11+
12+
class YouTubeApp(object):
13+
""" Youtube streaming application. """
14+
15+
def __init__(self):
16+
self.info = {
17+
'name' : 'YouTube',
18+
'path' : '/youtube',
19+
'description' : 'App to serve content from Youtube',
20+
'cover_image' : 'http://localhost:8080/res/images/y-music.jpeg',
21+
}
22+
23+
app_config = {
24+
'api_url' : 'https://www.googleapis.com/youtube/v3/',
25+
'api_key' : 'AIzaSyC5tw2VneJ1QhPQMTpqGL6yTxsQF2scI4A',
26+
'lang' : 'en',
27+
'region': 'BG',
28+
'max_results': 40
29+
}
30+
31+
self.dsn = server_config.load()['db.dsn']
32+
self.yt_objects = YoutubeObjects(self.dsn, app_config)
33+
self.data_api = YoutubeDataApi(self.yt_objects.config())
34+
35+
@cherrypy.expose
36+
def playlists(self, lid=None):
37+
"""
38+
List, add and delete youtube playlists.
39+
Add playlist with POST request and json payload { url: ... }.
40+
Del playlists with DELETE request and param id=(id of the playlist).
41+
:return a json output with all the playlists and details:
42+
43+
"""
44+
try:
45+
if cherrypy.request.method == 'GET':
46+
ids = self.yt_objects.playlists()
47+
if len(ids):
48+
return json.dumps(self.data_api.playlists(ids)).encode('utf8')
49+
elif cherrypy.request.method == 'POST':
50+
body = cherrypy.request.body.read()
51+
url = json.loads(body)['url']
52+
self.yt_objects.add_playlist(url)
53+
#cherrypy.response.status = '204 No Content'
54+
elif cherrypy.request.method == 'DELETE':
55+
if lid.isalnum():
56+
self.yt_objects.del_playlist(lid)
57+
else:
58+
raise cherrypy.HTTPError(400, message='InvalidList')
59+
except YoutubeObjectError as err:
60+
raise cherrypy.HTTPError(400, message=err.errdetail)
61+
62+
@cherrypy.expose
63+
def playlistItems(self, lid, pagetoken=None):
64+
"""
65+
Retrieve all of the playlist items.
66+
:param lid: unique id of the playlist.
67+
:param pagetoken: token to access a given result page with items.
68+
:return items and next page token if available:
69+
70+
"""
71+
# Don't care if playlist is in the DB so not checking.
72+
items, next_pagetoken = self.data_api.playlist_items(lid, pagetoken)
73+
return json.dumps({'pagetoken': next_pagetoken, 'items': items}).encode('utf8')
74+
75+
@cherrypy.expose
76+
def search(self, q):
77+
"""
78+
Search for any playlists or video.
79+
:param q search query can be any word or sentence:
80+
81+
"""
82+
res = self.data_api.search(q)
83+
return json.dumps(res).encode('utf8')
84+
85+
@cherrypy.expose
86+
def channels(self):
87+
"""
88+
List, add and delete youtube playlists.
89+
"""
90+
pass
91+
92+
@cherrypy.expose
93+
def play(self, v, mode):
94+
"""
95+
Stream youtube video/audio.
96+
:param v: id of the video to stream.
97+
:param mode: audio or video for the full multimedia.
98+
99+
"""
100+
if mode not in ('video', 'audio'):
101+
raise cherrypy.HTTPError(400, message='Invalid media mode')
102+
103+
with youtube_dl.YoutubeDL({'quiet': True}) as ydl:
104+
json_out = ydl.extract_info(v, download=False)
105+
106+
if mode == 'audio':
107+
streams = [f for f in json_out['formats'] if 'audio only' in f['format']]
108+
best_audio = sorted(streams, key=lambda audio: audio['abr'])[0]
109+
stream_url = best_audio['url']
110+
ext = best_audio['ext']
111+
elif mode == 'video':
112+
streams = [f for f in json_out['formats'] if f['vcodec'] != 'none' and f['acodec'] != 'none']
113+
stream_url = streams[0]['url']
114+
ext = streams[0]['ext']
115+
# Fetch youtube source stream
116+
resp = requests.get(stream_url, stream=True)
117+
if resp.status_code != requests.codes.ok:
118+
resp.raise_for_status()
119+
120+
cherrypy.response.headers.update({'Content-Type': '{}/{}'.format(mode, ext)})
121+
def content(resp):
122+
for chunk in resp.iter_content(chunk_size=10240):
123+
yield chunk
124+
resp.close()
125+
return content(resp)
126+
play._cp_config = {'response.stream': True}
127+
128+
@cherrypy.expose
129+
def download(self, v, mode):
130+
"""
131+
Download youtube video.
132+
:param v: id of the video to download.
133+
:param mode: audio or video for the full multimedia.
134+
135+
"""
136+
if mode not in ('video', 'audio'):
137+
raise cherrypy.HTTPError(400, message='Invalid media mode')
138+
139+
with youtube_dl.YoutubeDL({'quiet': True}) as ydl:
140+
json_out = ydl.extract_info(v, download=False)
141+
142+
video_streams = None
143+
if mode == 'audio':
144+
filename = json_out['title'] + '.webm'
145+
else:
146+
video_streams = [f for f in json_out['formats'] if f['vcodec'] != 'none' and f['acodec'] != 'none']
147+
filename = json_out['title'] + '.' + video_streams[0]['ext']
148+
149+
cherrypy.response.headers.update({'Content-Type': 'application/octet-stream',
150+
'Content-Disposition': 'attachment; filename=\"{}\"'.format(filename)})
151+
if mode == 'audio':
152+
ydl_opts = {
153+
'quiet': True,
154+
'outtmpl': '/tmp/%(title)s.%(ext)s',
155+
'format': 'bestaudio/best',
156+
'extractaudio' : True,
157+
'audioformat' : 'webm',
158+
}
159+
# Youtube has set a bandwith throttling on the audio streams,
160+
# so it's faster to download the full multimedia localy and extract the audio.
161+
filepath = os.path.join('/tmp', filename)
162+
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
163+
ydl.download([v])
164+
if not os.path.exists(filepath):
165+
raise cherrypy.HTTPError(404, message='Target file not found')
166+
def content(path):
167+
with open(path, 'rb') as fl:
168+
while True:
169+
chunk = fl.read(10240)
170+
if not chunk:
171+
break
172+
yield chunk
173+
os.unlink(path)
174+
return content(filepath)
175+
else:
176+
# Just restream video, not downloading multimedia localy.
177+
resp = requests.get(video_streams[0]['url'], stream=True)
178+
if resp.status_code != requests.codes.ok:
179+
resp.raise_for_status()
180+
def content(resp):
181+
for chunk in resp.iter_content(chunk_size=10240):
182+
yield chunk
183+
resp.close()
184+
return content(resp)
185+
download._cp_config = {'response.stream': True}
186+
187+

0 commit comments

Comments
 (0)