Browse Source

First commit

master
Chimo 1 year ago
commit
4fae33cf71
No known key found for this signature in database GPG Key ID: B0C36EDD0BB35A9C
5 changed files with 383 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +78
    -0
      private/main.py
  3. +30
    -0
      public/index.html
  4. +270
    -0
      public/js/script.js
  5. +3
    -0
      requirements.txt

+ 2
- 0
.gitignore View File

@ -0,0 +1,2 @@
private/config.json
venv

+ 78
- 0
private/main.py View File

@ -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)

+ 30
- 0
public/index.html View File

@ -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>

+ 270
- 0
public/js/script.js View File

@ -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 ) );

+ 3
- 0
requirements.txt View File

@ -0,0 +1,3 @@
flask
flask_cors
requests

Loading…
Cancel
Save