Skip to content
This repository was archived by the owner on May 28, 2022. It is now read-only.

Commit 957605f

Browse files
bohnjemtomwing
authored andcommitted
Close #435 Create HTTP API for controlling Freeseer recording
Adds a flask based server that can be launched from the CLI The server has a RESTful API for controlling the recording process The API currently supports: - initializing recordings - starting recordings - pausing recordings - stoping recordings - deleting recordings - getting information about recordings - getting a list of all available recordings There is also testing included to test the different API calls
1 parent e2fda9f commit 957605f

File tree

8 files changed

+866
-1
lines changed

8 files changed

+866
-1
lines changed

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
Flask==0.10.1
12
feedparser<5.2
23
google-api-python-client==1.2
4+
jsonschema==2.3
35
mutagen==1.22
46
oauth<1.1
57
simplejson<3.4

src/freeseer/frontend/cli.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def setup_parser():
6969
setup_parser_talk(subparsers)
7070
setup_parser_report(subparsers)
7171
setup_parser_upload(subparsers)
72+
setup_parser_server(subparsers)
7273
return parser
7374

7475

@@ -151,6 +152,12 @@ def setup_parser_upload_youtube(subparsers):
151152
parser.add_argument("-y", "--yes", help="Automatic yes to prompts", action="store_true")
152153

153154

155+
def setup_parser_server(subparsers):
156+
"""Setup server command parser"""
157+
parser = subparsers.add_parser("server", help="Setup a freeseer restful server")
158+
parser.add_argument("-f", "--filename", type=unicode, help="file to load recordings")
159+
160+
154161
def parse_args(parser, parse_args=None):
155162
if len(sys.argv) == 1: # No arguments passed
156163
launch_recordapp()
@@ -259,6 +266,12 @@ def parse_args(parser, parse_args=None):
259266
if args.upload_service == 'youtube':
260267
youtube.upload(args.files, args.token, args.yes)
261268

269+
elif args.app == 'server':
270+
if args.filename:
271+
launch_server(args.filename)
272+
else:
273+
launch_server()
274+
262275

263276
def launch_recordapp():
264277
"""Launch the Recording GUI if no arguments are passed"""
@@ -321,3 +334,10 @@ def launch_reporteditor():
321334
main = ReportEditorApp(config, db)
322335
main.show()
323336
sys.exit(app.exec_())
337+
338+
339+
def launch_server(storage_file="recording_storage"):
340+
"""Launch the Server"""
341+
import freeseer.frontend.controller.server as server
342+
343+
server.start_server(storage_file)

src/freeseer/frontend/controller/__init__.py

Whitespace-only changes.
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# freeseer - vga/presentation capture software
5+
#
6+
# Copyright (C) 2014 Free and Open Source Software Learning Centre
7+
# http://fosslc.org
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation, either version 3 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# This program is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
# For support, questions, suggestions or any other inquiries, visit:
23+
# http://wiki.github.com/Freeseer/freeseer/
24+
25+
import functools
26+
import json
27+
import os
28+
import signal
29+
import sys
30+
31+
from flask import current_app
32+
from flask import Flask
33+
from flask import jsonify
34+
from flask import request
35+
36+
from freeseer import settings
37+
from freeseer.framework.multimedia import Multimedia
38+
from freeseer.framework.plugin import PluginManager
39+
from freeseer.frontend.controller import validate
40+
41+
app = Flask(__name__)
42+
43+
44+
class HTTPError(Exception):
45+
46+
def __init__(self, message, status_code):
47+
super(HTTPError, self).__init__(message)
48+
self.message = message
49+
self.status_code = status_code
50+
51+
52+
class ServerError(Exception):
53+
54+
def __init__(self, message):
55+
super(ServerError, self).__init__(message)
56+
57+
58+
def catch_exceptions(func):
59+
@functools.wraps(func)
60+
def wrapper(*args, **kwargs):
61+
try:
62+
return func(*args, **kwargs)
63+
except HTTPError as e:
64+
return e.message, e.status_code
65+
return wrapper
66+
67+
68+
@app.route('/recordings', methods=['GET'])
69+
def get_all_recordings():
70+
response = jsonify({'recordings': current_app.media_dict.keys()})
71+
response.status_code = 200
72+
return response
73+
74+
75+
@app.route('/recordings/<int:recording_id>', methods=['GET'])
76+
@catch_exceptions
77+
def get_specific_recording(recording_id):
78+
response = ''
79+
80+
recording_id = int(recording_id)
81+
if recording_id in current_app.media_dict:
82+
retrieved_media_entry = current_app.media_dict[recording_id]
83+
retrieved_media = retrieved_media_entry['media']
84+
retrieved_filename = retrieved_media_entry['filename']
85+
86+
state_indicator = retrieved_media.current_state
87+
if state_indicator == Multimedia.NULL:
88+
state = 'NULL'
89+
elif state_indicator == Multimedia.RECORD:
90+
state = 'RECORD'
91+
elif state_indicator == Multimedia.PAUSE:
92+
state = 'PAUSE'
93+
elif state_indicator == Multimedia.STOP:
94+
state = 'STOP'
95+
else:
96+
raise HTTPError('recording state could not be determined', 500)
97+
98+
if os.path.isfile(retrieved_media_entry['filepath']):
99+
filesize = os.path.getsize(retrieved_media_entry['filepath'])
100+
else:
101+
filesize = 'NA'
102+
103+
response = jsonify({
104+
'id': recording_id,
105+
'filename': retrieved_filename,
106+
'filesize': filesize,
107+
'status': state
108+
})
109+
response.status_code = 200
110+
111+
else:
112+
raise HTTPError('recording id could not be found', 404)
113+
114+
return response
115+
116+
117+
@app.route('/recordings/<int:recording_id>', methods=['PATCH'])
118+
@catch_exceptions
119+
def control_recording(recording_id):
120+
response = ''
121+
122+
recording_id = int(recording_id)
123+
if recording_id in current_app.media_dict:
124+
retrieved_media_entry = current_app.media_dict[recording_id]
125+
retrieved_media = retrieved_media_entry['media']
126+
if validate.validate_control_recording_request_form(request.form):
127+
command = request.form['command']
128+
media_state = retrieved_media.current_state
129+
130+
if command == 'start' and media_state in [Multimedia.NULL, Multimedia.PAUSE]:
131+
retrieved_media.record()
132+
response = '', 200
133+
elif command == 'pause' and media_state == Multimedia.RECORD:
134+
retrieved_media.pause()
135+
response = '', 200
136+
elif command == 'stop' and media_state in [Multimedia.RECORD, Multimedia.PAUSE]:
137+
retrieved_media.stop()
138+
response = '', 200
139+
else:
140+
raise HTTPError('command could not be performed', 400)
141+
else:
142+
raise HTTPError('Form data was invalid', 400)
143+
144+
else:
145+
raise HTTPError('recording id could not be found', 404)
146+
147+
return response
148+
149+
150+
@app.route('/recordings', methods=['POST'])
151+
@catch_exceptions
152+
def create_recording():
153+
response = ''
154+
155+
if validate.validate_create_recording_request_form(request.form):
156+
new_filename = request.form['filename']
157+
new_media = Multimedia(current_app.record_config, current_app.record_plugin_manager)
158+
success, filename = new_media.load_backend(None, new_filename)
159+
160+
if success:
161+
filepath = new_media.plugman.get_plugin_by_name(new_media.config.record_to_file_plugin, "Output").plugin_object.location
162+
163+
new_recording_id = current_app.next_id
164+
current_app.next_id = current_app.next_id + 1
165+
166+
if new_recording_id not in current_app.media_dict:
167+
current_app.media_dict[new_recording_id] = {
168+
'media': new_media,
169+
'filename': filename,
170+
'filepath': filepath
171+
}
172+
173+
response = jsonify({'id': new_recording_id})
174+
response.status_code = 201
175+
else:
176+
raise HTTPError('Provided id already in use', 500)
177+
else:
178+
raise HTTPError('Could not load multimedia backend', 500)
179+
else:
180+
raise HTTPError('Form data was invalid', 400)
181+
182+
return response
183+
184+
185+
@app.route('/recordings/<int:recording_id>', methods=['DELETE'])
186+
@catch_exceptions
187+
def delete_recording(recording_id):
188+
recording_id = int(recording_id)
189+
if recording_id in current_app.media_dict:
190+
retrieved_media_entry = current_app.media_dict[recording_id]
191+
retrieved_media = retrieved_media_entry['media']
192+
193+
if retrieved_media.current_state == Multimedia.RECORD or retrieved_media.current_state == Multimedia.PAUSE:
194+
retrieved_media.stop()
195+
196+
# Delete the file if it exists
197+
if os.path.isfile(retrieved_media_entry['filepath']):
198+
os.remove(retrieved_media_entry['filepath'])
199+
200+
del current_app.media_dict[recording_id]
201+
response = '', 200
202+
else:
203+
raise HTTPError('recording id could not be found', 404)
204+
205+
return response
206+
207+
208+
def exit_gracefully(signum, frame):
209+
persistant = {}
210+
211+
# transfer the file information into the dictionary
212+
for key in app.media_dict:
213+
entry = app.media_dict[key]
214+
entry_media = entry['media']
215+
216+
# stop the recording if it is in progress or paused
217+
if entry_media.current_state == Multimedia.RECORD or entry_media.current_state == Multimedia.PAUSE:
218+
entry_media.stop()
219+
220+
persistant[key] = {
221+
'filename': entry['filename'],
222+
'filepath': entry['filepath'],
223+
'status': entry_media.current_state
224+
}
225+
226+
with open(app.storage_file, 'w') as fd:
227+
fd.write(json.dumps(persistant))
228+
229+
sys.exit(1)
230+
231+
232+
def configure(storage_file):
233+
app.record_profile = settings.profile_manager.get()
234+
app.record_config = app.record_profile.get_config('freeseer.conf', settings.FreeseerConfig, ['Global'], read_only=True)
235+
app.record_plugin_manager = PluginManager(app.record_profile)
236+
app.storage_file = os.path.join(settings.configdir, storage_file)
237+
app.next_id = 1
238+
239+
# restore talks from storage
240+
if os.path.isfile(app.storage_file):
241+
with open(app.storage_file) as fd:
242+
persistant = json.loads(fd.read())
243+
244+
app.media_dict = {}
245+
for key in persistant:
246+
new_media = Multimedia(app.record_config, app.record_plugin_manager)
247+
new_media.current_state = persistant[key]['status']
248+
int_key = int(key)
249+
250+
if new_media.current_state == Multimedia.NULL:
251+
filename = persistant[key]['filename'].split(".ogg")[0]
252+
success, filename = new_media.load_backend(None, filename)
253+
254+
if success:
255+
filepath = new_media.plugman.get_plugin_by_name(new_media.config.record_to_file_plugin, "Output").plugin_object.location
256+
app.media_dict[int_key] = {
257+
'media': new_media,
258+
'filename': filename,
259+
'filepath': filepath
260+
}
261+
else:
262+
raise ServerError('Could not load multimedia backend')
263+
else:
264+
app.media_dict[int_key] = {
265+
'media': new_media,
266+
'filename': persistant[key]['filename'],
267+
'filepath': persistant[key]['filepath']
268+
}
269+
270+
if int_key >= app.next_id:
271+
app.next_id = int_key + 1
272+
else:
273+
app.media_dict = {}
274+
275+
276+
def start_server(storage_file):
277+
# setup the application so it exits gracefully
278+
signal.signal(signal.SIGINT, exit_gracefully)
279+
280+
configure(storage_file)
281+
app.run()

0 commit comments

Comments
 (0)