First commit
commit
4fae33cf71
@ -0,0 +1,2 @@
|
||||
private/config.json
|
||||
venv
|
@ -0,0 +1,78 @@
|
||||
from flask import Flask, jsonify, request
|
||||
import requests
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
with open('config.json') as fd:
|
||||
config = json.load(fd)
|
||||
app.config['root_url'] = config['root_url']
|
||||
app.config['username'] = config['username']
|
||||
app.config['password'] = config['password']
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return 'Hi.'
|
||||
|
||||
@app.route('/subscriptions')
|
||||
def subscriptions():
|
||||
root_url = app.config['root_url']
|
||||
username = app.config['username']
|
||||
password = app.config['password']
|
||||
endpoint = root_url + f"/subscriptions/{username}.json"
|
||||
|
||||
r = requests.get(endpoint, auth=(username, password))
|
||||
content = r.json()
|
||||
|
||||
return jsonify(content)
|
||||
|
||||
@app.route('/podcast/<path:sub_id>/episodes')
|
||||
def episodes(sub_id):
|
||||
root_url = app.config['root_url']
|
||||
username = app.config['username']
|
||||
password = app.config['password']
|
||||
|
||||
# params = {'podcast': sub_id, 'aggregated': 'true'}
|
||||
params = {'podcast': sub_id}
|
||||
endpoint = root_url + f"/api/2/episodes/{username}.json"
|
||||
|
||||
r = requests.get(endpoint, auth=(username, password), params=params)
|
||||
content = r.json()
|
||||
|
||||
return jsonify(content)
|
||||
|
||||
@app.route('/subscription/<path:sub_id>/episode/<path:ep_id>', methods=['POST'])
|
||||
def episode(sub_id, ep_id):
|
||||
root_url = app.config['root_url']
|
||||
username = app.config['username']
|
||||
password = app.config['password']
|
||||
|
||||
json = request.json
|
||||
|
||||
action = json['action']
|
||||
started = json['started']
|
||||
position = json['position']
|
||||
total = json['total']
|
||||
|
||||
params = [{
|
||||
'podcast': sub_id,
|
||||
'episode': ep_id,
|
||||
# 'device': 'web',
|
||||
'action': action,
|
||||
'started': started,
|
||||
'position': position,
|
||||
'total': total
|
||||
}]
|
||||
|
||||
endpoint = root_url + f"/api/2/episodes/{username}.json"
|
||||
|
||||
r = requests.post(endpoint, json=params, auth=(username, password))
|
||||
content = r.json()
|
||||
|
||||
return jsonify(content)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
|
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<style>
|
||||
.hidden { display: none; }
|
||||
|
||||
.episode-finished { opacity: 0.3; }
|
||||
.episode-started { color: green; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="audio-player">
|
||||
<audio id="audio" preload="none" controls="controls"></audio>
|
||||
</div>
|
||||
|
||||
<div id="subscriptions-panel">
|
||||
<ul id="subscriptions"></ul>
|
||||
</div>
|
||||
|
||||
<div id="episodes-panel">
|
||||
<ul id="episodes"></ul>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -0,0 +1,270 @@
|
||||
// Throwaway code to explore what we'll need from a 'view'
|
||||
// perspective, which endpoints we need to hit/create, etc.
|
||||
( function( doc ) {
|
||||
let subsElm = doc.querySelector( "#subscriptions" ),
|
||||
rootUrl = "http://localhost:5000",
|
||||
subscriptionsEndpoint = rootUrl + "/subscriptions",
|
||||
|
||||
secondsToHuman = function( secs ) {
|
||||
let hours = leftPad( Math.floor( secs / 3600 ), 2, "0" ),
|
||||
minutes = leftPad( Math.floor( secs % 3600 / 60 ), 2, "0" ),
|
||||
seconds = leftPad( secs % 60, 2, "0" );
|
||||
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
},
|
||||
|
||||
leftPad = function( str, width, filler ) {
|
||||
let strLength = str.toString().length,
|
||||
diff = width - strLength,
|
||||
padded = str;
|
||||
|
||||
while( diff > 0 ) {
|
||||
padded = filler + padded;
|
||||
diff -= 1;
|
||||
}
|
||||
|
||||
return padded;
|
||||
},
|
||||
|
||||
playEpisode = function( episode ) {
|
||||
var audioElm = doc.querySelector( "#audio" );
|
||||
|
||||
audioElm.setAttribute( "src", episode.url );
|
||||
audioElm.setAttribute( "preload", "none" );
|
||||
audioElm.setAttribute( "controls", "controls" );
|
||||
|
||||
if ( episode.chr.position ) {
|
||||
audioElm.currentTime = episode.chr.position;
|
||||
}
|
||||
|
||||
audioElm.dataset.episode = JSON.stringify( episode) ;
|
||||
|
||||
audioElm.playbackRate = 1.75;
|
||||
audioElm.play();
|
||||
},
|
||||
|
||||
wrapup = function( episodes ) {
|
||||
let episodesElm = doc.querySelector( "#episodes" );
|
||||
|
||||
// Sort by release dates
|
||||
episodes.sort( function( a, b ) {
|
||||
return a.released < b.released;
|
||||
} );
|
||||
|
||||
episodes.forEach( ( episode ) => {
|
||||
let listItem = doc.createElement( "li" ),
|
||||
playButton = doc.createElement( "button" ),
|
||||
title = doc.createElement( "a" ),
|
||||
currentPosition = "?",
|
||||
trackLength = "?",
|
||||
episodeState = "episode-not-started";
|
||||
|
||||
if ( episode.chr.position ) {
|
||||
currentPosition =
|
||||
secondsToHuman( episode.chr.position );
|
||||
}
|
||||
|
||||
if ( episode.chr.total ) {
|
||||
trackLength = secondsToHuman( episode.chr.total );
|
||||
}
|
||||
|
||||
if ( currentPosition !== "?" ) {
|
||||
if ( currentPosition === trackLength ) {
|
||||
episodeState = "episode-finished";
|
||||
} else {
|
||||
episodeState = "episode-started";
|
||||
}
|
||||
}
|
||||
|
||||
listItem.classList.add( episodeState );
|
||||
|
||||
title.textContent = episode.title +
|
||||
` (${currentPosition}/${trackLength})`;
|
||||
|
||||
playButton.textContent = "Play";
|
||||
playButton.addEventListener( "click", function() {
|
||||
playEpisode( episode );
|
||||
} );
|
||||
|
||||
listItem.appendChild( title );
|
||||
listItem.appendChild( playButton );
|
||||
|
||||
episodesElm.appendChild( listItem );
|
||||
} );
|
||||
},
|
||||
|
||||
getEpisodeDetails = function( episode ) {
|
||||
let episodeUrl = encodeURIComponent( episode.episode ),
|
||||
podcastUrl = encodeURIComponent( episode.podcast ),
|
||||
endpoint = "https://podcasts.chromic.org" +
|
||||
`/api/2/data/episode.json?podcast=${podcastUrl}&url=${episodeUrl}`;
|
||||
|
||||
return fetch( endpoint )
|
||||
.then( ( response ) => {
|
||||
return response.json();
|
||||
} )
|
||||
.then( ( json ) => {
|
||||
json.chr = {
|
||||
position: episode.position,
|
||||
total: episode.total,
|
||||
started: episode.started
|
||||
};
|
||||
|
||||
return Promise.resolve( json );
|
||||
} )
|
||||
.catch( ( err ) => {
|
||||
return Promise.reject( err );
|
||||
} );
|
||||
},
|
||||
|
||||
processEpisodes = function( episodes ) {
|
||||
let uniqEpisodes = [],
|
||||
filledUniqEpisodes = [],
|
||||
episodesDetailsParam = [],
|
||||
processed = 0,
|
||||
nbEpisodes = 0;
|
||||
|
||||
subsElm.classList.add( "hidden" );
|
||||
|
||||
episodes.actions.forEach( ( action ) => {
|
||||
let episode = uniqEpisodes[ action.episode ];
|
||||
|
||||
if ( typeof episode === "undefined"
|
||||
|| episode.timestamp < action.timestamp ) {
|
||||
// We've seen this episode before, but this is a
|
||||
// more recent action so overwrite whatever we had
|
||||
// before.
|
||||
uniqEpisodes[ action.episode ] = action
|
||||
|
||||
// Episode we haven't catalogued before
|
||||
if ( typeof episode === "undefined" ) {
|
||||
nbEpisodes += 1;
|
||||
episodesDetailsParam.push( action.episode );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
// Iterate over keys since this is now a "associative
|
||||
// array" i.e.: JS object
|
||||
//
|
||||
// It also doesn't have a .length property, which is why we
|
||||
// keep track of the number of episodes in a separate
|
||||
// variable (nbEpisodes)
|
||||
//
|
||||
// `this` is `uniqEpisodes` since it's being passed as the
|
||||
// 2nd parameter to forEach
|
||||
//
|
||||
// Wait until we get all the data before displaying
|
||||
// because we want to sort the list of episodes by
|
||||
// released date.
|
||||
Object.keys( uniqEpisodes ).forEach( function( key ) {
|
||||
let episode = this[ key ];
|
||||
|
||||
getEpisodeDetails( episode )
|
||||
.then( ( filled ) => {
|
||||
filledUniqEpisodes.push( filled );
|
||||
processed += 1;
|
||||
} )
|
||||
.catch( ( err ) => {
|
||||
processed += 1;
|
||||
} )
|
||||
.finally( () => {
|
||||
if( processed >= nbEpisodes ) {
|
||||
wrapup( filledUniqEpisodes );
|
||||
}
|
||||
} );
|
||||
}, uniqEpisodes );
|
||||
},
|
||||
|
||||
getEpisodes = function( e ) {
|
||||
e.preventDefault();
|
||||
|
||||
// TODO: Update URL via history API, because why not.
|
||||
|
||||
let endpoint = this.getAttribute( "href" );
|
||||
|
||||
fetch( endpoint )
|
||||
.then( ( response ) => {
|
||||
return response.json();
|
||||
} )
|
||||
.then( processEpisodes );
|
||||
},
|
||||
|
||||
getSubscriptions = function() {
|
||||
fetch( "http://localhost:5000/subscriptions" )
|
||||
.then( ( response ) => {
|
||||
return response.json();
|
||||
} )
|
||||
.then( ( json ) => {
|
||||
json.forEach( ( podcast ) => {
|
||||
let listItem = doc.createElement( "li" ),
|
||||
link = doc.createElement( "a" ),
|
||||
subId = encodeURIComponent( podcast.url ),
|
||||
href = rootUrl + `/podcast/${subId}/episodes`
|
||||
|
||||
link.setAttribute( "href", href );
|
||||
link.textContent = podcast.title;
|
||||
link.addEventListener( "click", getEpisodes );
|
||||
|
||||
listItem.appendChild( link );
|
||||
subsElm.appendChild( listItem );
|
||||
} );
|
||||
} );
|
||||
},
|
||||
|
||||
setupAudio = function() {
|
||||
let audioElm = doc.querySelector( "#audio" );
|
||||
|
||||
audioElm.addEventListener( "ended", postProgress );
|
||||
audioElm.addEventListener( "pause", postProgress );
|
||||
audioElm.addEventListener( "play", trackProgress );
|
||||
},
|
||||
|
||||
trackProgress = function() {
|
||||
let audioElm = this,
|
||||
episode = JSON.parse( audioElm.dataset.episode ),
|
||||
chr = {
|
||||
started: audioElm.currentTime,
|
||||
total: audioElm.duration
|
||||
};
|
||||
|
||||
episode.chr = chr;
|
||||
|
||||
audioElm.dataset.episode = JSON.stringify( episode );
|
||||
},
|
||||
|
||||
postProgress = function() {
|
||||
let audioElm = this,
|
||||
episode = JSON.parse( audioElm.dataset.episode ),
|
||||
podcastUrl = episode.podcast_url,
|
||||
episodeUrl = episode.url,
|
||||
endpoint = rootUrl +
|
||||
`/subscription/${podcastUrl}/episode/${episodeUrl}`,
|
||||
payload = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
action: "play",
|
||||
started: Math.floor( episode.chr.started ),
|
||||
// total: Math.floor( episode.chr.total ),
|
||||
total: Math.floor( audioElm.duration ),
|
||||
position: Math.floor( audioElm.currentTime )
|
||||
} )
|
||||
};
|
||||
|
||||
fetch( endpoint, payload )
|
||||
.then( ( response ) => {
|
||||
return response.json();
|
||||
} )
|
||||
.then( ( json ) => {
|
||||
console.log( json );
|
||||
} );
|
||||
};
|
||||
|
||||
getSubscriptions();
|
||||
setupAudio();
|
||||
|
||||
}( document ) );
|
||||
|
@ -0,0 +1,3 @@
|
||||
flask
|
||||
flask_cors
|
||||
requests
|
Loading…
Reference in New Issue