Verified Commit 508ae9aa authored by Stephan Fischer's avatar Stephan Fischer
Browse files

First initial release. Wow, that only took some months ;)

parents
The MIT License (MIT)
Copyright (c) 2015-2016 Stephan Fischer ([tocsin.de](https://tocsin.de), [stephan-fischer.de](https://stephan-fischer.de))
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### Used scripts
* [Flask](https://github.com/pallets/flask) (Armin Ronacher and contributors, BSD License)
* [Video.js](https://github.com/videojs/video.js) (Brightcove Inc, Apache License 2.0)
* [videojs-youtube](https://github.com/videojs/videojs-youtube) (Benoit Tremblay, MIT License)
* [videojs-vimeo](https://github.com/videojs/videojs-vimeo) (Benoit Tremblay, MIT License)
# newsic
*Hey there, this is an early version of newsic, which might catch fire quite often. You're very welcome to open an [issue](https://github.com/newsic/newsic/issues) for bugs you found, improvements or new ideas :)*
Got YouTube playlists with 100+ songs and a thirst for exploring something new? With newsic you can automatically listen to small 30-second-snippets and hear full songs if you'd like to jump into the tune.
## Project page & demo
[Project page: newsic.tocsin.de](https://newsic.tocsin.de)
[Demo: trynewsic.tocsin.de](https://trynewsic.tocsin.de)
## How newsic looks like
![How newsic looks like.](https://newsic.tocsin.de/stc/github/demo_2016-10-02.png)
## System requirements
newsic's backend is based on Python 3 and the [Flask](https://github.com/pallets/flask) framework, while the frontend uses [Video.js](https://github.com/videojs/video.js) (which comes bundled with this repository).
Flask (and Jinja2 and Werkzeug) can either be installed with your favorite package manager or with `pip install -r requirements.txt`
## Usage
1. Clone this repository.
2. Open the config.py and add your YouTube API key (which you can request [here](https://console.developers.google.com/apis/api/youtube/)).
3. Navigate to the folder where you cloned the code and start newsic with `python newsic.py`
4. newsic is now available at 127.0.0.1:5000
### Deployment
For running newsic on shared hosting provider you would like to use a deployment script.
There are many [different options for running Flask on a server](http://flask.pocoo.org/docs/dev/deploying/).
The code running at [trynewsic.tocsin.de](https://trynewsic.tocsin.de) (hosted proudly on Uberspace.de) is released at [newsic/deployment](https://github.com/newsic/deployment).
### Shortcuts
Shortcut | What will happen (hopefully)
------------ | -------------
`k` or `Space bar` | Play/Pause
`a` or `鈫恅 | Previous song
`d` or `鈫抈 | Next song
`c` | Play complete song
`f` | Toggle fullscreen
## License
MIT License, Copyright (c) 2015-2016 Stephan Fischer ([tocsin.de](https://tocsin.de), [stephan-fischer.de](https://stephan-fischer.de))
For more details see [LICENSE.md](/LICENSE.md).
class general():
# YouTube API configuration (you can request a token at https://console.developers.google.com/apis/api/youtube/)
YOUTUBE_API_KEY = ""
# Vimeo API configuration (get your tokens at https://developer.vimeo.com/apps)
VIMEO_TOKEN = ""
VIMEO_KEY = ""
VIMEO_SECRET = ""
# snippet length in seconds (default: 30)
SNIPPETLENGTH = 30
class local():
# Flask debug
DEBUG = True
# for more detailed, pure newsic-related, debug messages
NEWSICDEBUG = True
# for deployment on specific hosts (like Uberspace.de)
# fixes urls automatically generated by Flask (caused by our fcgi-bin script)
FCGI_PATH = False
class server():
DEBUG = False
NEWSICDEBUG = False
FCGI_PATH = "fcgi-bin/newsic.fcgi/"
from flask import Flask, render_template, request, redirect, url_for
from re import compile
from urllib import request as urlrequest
from json import loads
from math import fmod
from vimeo import VimeoClient
app = Flask(__name__)
# config imports
app.config.from_object("config.general")
app.config.from_object("config.local")
#app.config.from_object("config.server")
def fcgiPath():
if app.config["FCGI_PATH"]:
return app.config["FCGI_PATH"]
else:
return ""
def debug(text):
if app.config["NEWSICDEBUG"]:
print(text)
# fix redirect urls (for deployment via fcgi-bin script)
def strip_url(orig):
return orig.replace(fcgiPath(), "")
# index page
@app.route("/")
def index():
return render_template(
"index.html",
fcgiPath = fcgiPath(),
bodyClass = "home",
title = "Home"
)
# check of input url (redirects to index page )
@app.route("/play", methods = ["POST"])
@app.route("/", methods = ["POST"])
def index_POST():
url = request.form["url"]
# check for youtube urls (playlists)
youtube = compile(r"(?:https?:\/\/)*(?:w{0,3}|m).?youtube.com\/.*list=(.+)")
vimeo = compile(r"(?:https?:\/\/)*(?:w{0,3}).?vimeo.com\/.*album\/(\d+)")
yt_playlist = youtube.match(url)
vim_playlist = vimeo.match(url)
if(yt_playlist):
debug(("Found a YouTube playlist: {0}").format(yt_playlist.group(1)))
if(fcgiPath()):
return redirect(strip_url(url_for(
"play_youtube",
youtubePlaylist = yt_playlist.group(1)
)))
else:
return redirect(url_for(
"play_youtube",
youtubePlaylist = yt_playlist.group(1)
))
if(vim_playlist):
debug(("Found a Vimeo playlist: {0}").format(vim_playlist.group(1)))
if(fcgiPath()):
return redirect(strip_url(url_for(
"play_vimeo",
vimeoPlaylist = vim_playlist.group(1)
)))
else:
return redirect(url_for(
"play_vimeo",
vimeoPlaylist = vim_playlist.group(1)
))
# redirect to index page in case there's no valuable user input
else:
debug("No music found")
return render_template(
"index.html",
error = "No music found.",
fcgiPath = fcgiPath(),
bodyClass = "home",
title = "No music found"
)
# play some music from YouTube (currently only works with playlists <= 50 videos)
@app.route("/play/youtube/<youtubePlaylist>")
def play_youtube(youtubePlaylist):
if youtubePlaylist:
maxResults = 50
videoIds = []
videolist = []
i = 0
# fetch general information about the playlist
api_playlist = ("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id={0}&fields=items&key={1}").format(youtubePlaylist, app.config["YOUTUBE_API_KEY"])
response_playlist = urlrequest.urlopen(api_playlist)#.decode("utf-8")
data_playlist = loads(response_playlist.read().decode())
#TODO: check if playlist is private/not listed etc..
playlistTitle = data_playlist["items"][0]["snippet"]["title"]
playlistCreator = data_playlist["items"][0]["snippet"]["channelTitle"]
# fetch video ids from playlist
requestVideoIds = ("https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails&playlistId={0}&fields=items%2FcontentDetails%2CnextPageToken%2CpageInfo&key={1}&maxResults={2}").format(youtubePlaylist, app.config["YOUTUBE_API_KEY"], maxResults)
receiveVideoIds = urlrequest.urlopen(requestVideoIds)#.decode("utf-8")
jsonVideoIds = loads(receiveVideoIds.read().decode())
for items in jsonVideoIds["items"]:
videoIds.append(items["contentDetails"]["videoId"])
while "nextPageToken" in jsonVideoIds:
requestVideoIds = ("https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails&playlistId={0}&fields=items%2FcontentDetails%2CnextPageToken%2CpageInfo&key={1}&maxResults={2}&pageToken={3}").format(youtubePlaylist, app.config["YOUTUBE_API_KEY"], maxResults, jsonVideoIds["nextPageToken"])
receiveVideoIds = urlrequest.urlopen(requestVideoIds)#.decode("utf-8")
jsonVideoIds = loads(receiveVideoIds.read().decode())
for items in jsonVideoIds["items"]:
videoIds.append(items["contentDetails"]["videoId"])
iterations = round(len(videoIds) / maxResults, 0)
modulo = fmod(len(videoIds), maxResults)
if modulo > 0 and iterations > 1:
iterations = iterations + 1
#debug(iterations * maxResults)
#debug(iterations)
# todo: while clause here
videoIds_part = ','.join(videoIds[:50])
url = ("https://www.googleapis.com/youtube/v3/videos?part=contentDetails,snippet,status&id={0}&fields=items(contentDetails,snippet,status)&key={1}").format(videoIds_part, app.config["YOUTUBE_API_KEY"])
response = urlrequest.urlopen(url)#.decode("utf-8")
data = loads(response.read().decode())
# iterate through videolist and use every video id to fetch more details
# todo: we need to iterate again right here#
for video in videoIds[:50]:
if data["items"]:
debug(i)
debug(len(videoIds))
embedStatus = data["items"][i]["status"]["embeddable"]
privacyStatus = data["items"][i]["status"]["privacyStatus"]
uploadStatus = data["items"][i]["status"]["uploadStatus"]
if(embedStatus and (privacyStatus == "public" or privacyStatus == "unlisted") and uploadStatus == "processed"):
blocked = []
allowed = []
debug(("\nCurrent video: {0} (ID: {1}) \nPrivacy: {2} | Upload status: {3}").format(data["items"][i]["snippet"]["title"], video, privacyStatus, uploadStatus))
# make sure we know where videos are not available (important for later use in template "play")
if "regionRestriction" in data["items"][i]["contentDetails"]:
if "blocked" in data["items"][i]["contentDetails"]["regionRestriction"]:
for country in data["items"][i]["contentDetails"]["regionRestriction"]["blocked"]:
blocked.append(country)
debug(("Blocked in these countries: {0}").format(str(blocked)))
if "allowed" in data["items"][i]["contentDetails"]["regionRestriction"]:
for country in data["items"][i]["contentDetails"]["regionRestriction"]["allowed"]:
allowed.append(country)
debug(("Only allowed in these countries: {0}").format(str(allowed)))
# convert YouTube's time format to hours, minutes and seconds
hours = minutes = seconds = 0
# todo: improve regex
length_raw = compile(r"PT(?P<h>\d*H)*(?P<m>\d*M)*(?P<s>\d*S)*")
length = length_raw.match(data["items"][i]["contentDetails"]["duration"])
if length.group("h") is not None:
hours = length.group("h").replace("H", "")
if length.group("m") is not None:
minutes = length.group("m").replace("M", "")
if length.group("s") is not None:
seconds = length.group("s").replace("S", "")
length_in_sec = int(hours) * 60 * 60 + int(minutes) * 60 + int(seconds)
timeMiddle = length_in_sec / 2
videolist.append([
video,
timeMiddle,
timeMiddle + app.config["SNIPPETLENGTH"],
data["items"][i]["snippet"]["title"],
blocked,
allowed,
length_in_sec
])
else:
debug("Embedding is not allowed, so this video was skipped.")
i = i + 1
return render_template(
"youtube.html",
videolist = videolist,
playlistTitle = playlistTitle,
playlistCreator = playlistCreator,
playlistVideoAmount = len(videolist),
playlistLength = int(float((len(videolist) * app.config["SNIPPETLENGTH"]) / 60)),
fcgiPath = fcgiPath(),
title = "Loading..."
)
# heavy in development!
@app.route("/play/vimeo/<vimeoPlaylist>")
def play_vimeo(vimeoPlaylist):
videolist = []
v = VimeoClient(
token = app.config["VIMEO_TOKEN"],
key = app.config["VIMEO_KEY"],
secret = app.config["VIMEO_SECRET"])
rawResponse_vids = v.get(('/albums/{0}/videos').format(vimeoPlaylist))
rawResponse_general = v.get(('/albums/{0}').format(vimeoPlaylist))
regex_ids = compile(r"\/videos\/(\d+)\/pictures\/(\d+)")
fetchedVideos = rawResponse_vids.json()
fetchedGeneralData = rawResponse_general.json()
playlistTitle = fetchedGeneralData["name"]
playlistCreator = fetchedGeneralData["user"]["name"]
for data in fetchedVideos["data"]:
ids = regex_ids.match(data["pictures"]["uri"])
if(ids):
title = data["name"]
length_in_sec = data["duration"]
timeMiddle = length_in_sec / 2
videoId = ids.group(1)
thumbnailId = ids.group(2)
videolist.append([
videoId,
thumbnailId,
timeMiddle,
timeMiddle + app.config["SNIPPETLENGTH"],
title,
length_in_sec
])
return render_template("vimeo.html", videolist = videolist, playlistTitle = playlistTitle, playlistCreator = playlistCreator, playlistVideoAmount = len(videolist), playlistLength = int(float((len(videolist) * app.config["SNIPPETLENGTH"]) / 60)), fcgiPath = fcgiPath(), title = "Loading...")
@app.errorhandler(404)
def four0four(error):
return render_template("index.html", error = "Page not found.", fcgiPath = fcgiPath(), bodyClass = "home", title = "Page not found"), 404
if __name__ == "__main__":
app.run(debug = app.config["DEBUG"])
/*
_
(_)
_ __ _____ _____ _ ___
| '_ \ / _ \ \ /\ / / __| |/ __|
| | | | __/\ V V /\__ \ | (__
|_| |_|\___| \_/\_/ |___/_|\___|
=================================
Howdy, dear source code reader!
* Official demo version: https://newsic.tocsin.de
* newsic on GitHub: https://github.com/newsic/newsic
* Author: Stephan Fischer (https://stephan-fischer.de)
*/
* {
box-sizing: border-box;
}
body {
font-family: "Raleway", sans-serif;
background: #4d4d4d;
margin: 0;
color: #A3A3A3;
font-size:1em;
}
header {
background: #1d1d1d;
padding-left: 15%;
padding-right: 15%;
overflow: auto;
padding-bottom: 1%;
padding-top: 2%;
}
footer, .lines {
margin-top: 30px;
background: #595959;
padding: 3px 0px 4px 2px;
margin-bottom: 30px;
}
.lines div, footer div {
margin-left: -5px;
margin-top: -5px;
width: 100%;
height: 5px;
background: #666;
}
footer {
width: 70%;
margin: auto;
text-align: center;
font-size: 0.9em;
margin-bottom: 50px;
margin-top:5%;
}
footer a {
background: #4d4d4d;
border: 1px solid #808080;
padding: 5px 20px 5px 20px;
border-radius: 2px;
top: -5px;
position: relative;
}
#colorcountdown {
width: 100%;
height: 100%;
background: #BCD35F;
transition: width 0.5s linear;
text-align: center;
margin-left: 0;
}
#info {
visibility:hidden;
transition:visibility 2s, opacity 2s linear;
opacity:0;
background: #D3645F;
color: #fff;
text-align: center;
padding: 5px;
font-size: 0.9em;
}
.snippetinfo, .center {
background: #3d3d3d;
width: 70%;
margin: auto;
padding: 1% 10% 1.5% 10%;
margin-bottom: 2%;
margin-top: 2%;
color: #ccc;
text-align: center;
overflow: auto;
}
.center {
text-align:justify;
}
.snippetinfo span, .featured span {
border: 1px solid #7B7B7B;
border-radius: 2px;
padding: 5px 10px 5px 10px;
color: #A3A3A3;
font-size: 0.9em;
}
.snippetinfo .fl span {
margin-right: 20px;
}
.snippetinfo .fr span {
margin-left: 20px;
}
h1, h2 {
font-weight: bold;
}
h1 {
color: #fff;
line-height: 1em;
}
h2 {
color: #fff;
}
.videoBorder {
width: 70%;
margin: auto;
margin-top: 2%;
}
.videoWrapper {
position: relative;
padding-bottom: 56.25%;
height: 0;
}
.videoWrapper .video-js {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#seconds {
position: relative;
top: -15px;
background: #3d3d3d;
border-radius: 5px;
border: 1px solid #7B7B7B;
color: #fff;
display: inline-block;
font-weight:bold;
}
#snippets {
margin-left: 25%;
margin-top: 10px;
margin-right: 25%;
width: 50%;
}
#snippets div {
width: 25%;
display: inline-block;
vertical-align: top;
margin-bottom: 2%;
padding:1%;
transition: color 0.5s ease;
cursor: pointer;
}
#snippets img {
width: 100%;
opacity: 0.4;
margin-bottom: 2%;
transition: opacity 0.5s ease;
}
#snippets p {
opacity: 0.4;
margin-bottom: 2%;
transition: opacity 0.5s ease;
font-size:0.8em;
}
#snippets div:hover, #snippets .active div {
color: #fff;
transition: color 0.5s ease;
}
#snippets div:hover img, #snippets div:hover p, #snippets .active img, #snippets .active p {
opacity: 1;
transition: opacity 0.5s ease;
}
footer a, footer a:visited, footer .fa-heart {
transition: color 0.5s ease;
text-decoration: none;
color:#CACACA;
}
footer a:hover {
color: #fff;
transition: color 0.5s ease;
}
footer a:hover .fa-heart {
color:#D3645F;
transition: color 0.5s ease;
}
.message {
background: #3d3d3d;
color: #fff;
text-align: center;
padding: 5px;
font-size: 0.9em;
}
a, a:visited {
color:#fff;
.color: #BCD35F;
.font-weight: bold;
text-decoration: none;
transition: color 0.5s ease;
}
a:hover {
color: #fff;
text-decoration: underline;
transition: color 0.5s ease;
}
.message a {
font-weight:bold;
}
.controlbar {
margin-left: 25%;
margin-right: 25%;
margin-top: 2%;
margin-bottom: 2%;
}
p.error {
background: #D3645F;
color: #fff;
padding: 8px 10px 8px 10px;
font-size: 0.9em;
text-align: center;
}
/* shadow div - general */
.shadow div {
display: inline-block;
background: #595959;
padding: 0px 4px 4px 2px;
}
.shadow div >* {
margin-left: -5px;
margin-top: -5px;