PyCharm grand repo wide reformat

This commit is contained in:
Xevion
2020-03-08 20:21:18 -05:00
parent 6aeb41249c
commit 66c2ff228c
17 changed files with 277 additions and 187 deletions

View File

@@ -1,12 +1,13 @@
# Main Flask and Flask configs # Main Flask and Flask configs
from flask import Flask from flask import Flask
from config import Config
# Flask Extensions
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from flask_login import LoginManager
from flask_migrate import Migrate
# Flask Extensions
from flask_sqlalchemy import SQLAlchemy
from config import Config
# App & App config setup # App & App config setup
app = Flask(__name__) app = Flask(__name__)
@@ -21,4 +22,4 @@ limiter = Limiter(app, key_func=get_remote_address, default_limits=["10 per seco
from app import models from app import models
from app import routes, simple_routes, hidden, dashboard from app import routes, simple_routes, hidden, dashboard
from app import ftbhot, custom, spotify, panzer, sound from app import ftbhot, custom, spotify, panzer, sound

View File

@@ -1,6 +1,8 @@
from functools import wraps
from flask import abort from flask import abort
from flask_login import current_user from flask_login import current_user
from functools import wraps
def require_role(roles=["User"]): def require_role(roles=["User"]):
def wrap(func): def wrap(func):
@@ -10,5 +12,7 @@ def require_role(roles=["User"]):
if current_user.has_roles(roles): if current_user.has_roles(roles):
return func(*args, **kwargs) return func(*args, **kwargs)
return abort(401) return abort(401)
return decorated_view return decorated_view
return wrap
return wrap

View File

@@ -1,15 +1,17 @@
from app import app, db, login from flask import render_template, request, jsonify
from app.forms import ProfileSettingsForm, ProfilePictureForm from flask_login import login_required
from app.models import User, Search
from app import app
from app.custom import require_role from app.custom import require_role
from flask import render_template, redirect, url_for, request, jsonify from app.forms import ProfileSettingsForm, ProfilePictureForm
from flask_login import current_user, login_required
@app.route('/dashboard') @app.route('/dashboard')
@login_required @login_required
def dashboard(): def dashboard():
return render_template('/dashboard/dashboard.html') return render_template('/dashboard/dashboard.html')
@app.route('/dashboard/profile_settings', methods=['GET']) @app.route('/dashboard/profile_settings', methods=['GET'])
@login_required @login_required
def profile_settings(): def profile_settings():
@@ -17,25 +19,28 @@ def profile_settings():
ppform = ProfilePictureForm() ppform = ProfilePictureForm()
return render_template('/dashboard/profile_settings.html', psform=psform, ppform=ppform) return render_template('/dashboard/profile_settings.html', psform=psform, ppform=ppform)
@app.route('/dashboard/profile_settings/submit', methods=['POST']) @app.route('/dashboard/profile_settings/submit', methods=['POST'])
@login_required @login_required
def profile_settings_submit(): def profile_settings_submit():
form = ProfileSettingsForm() form = ProfileSettingsForm()
if form.validate_on_submit(): if form.validate_on_submit():
data = { data = {
'show_email' : form.show_email.data or None, 'show_email': form.show_email.data or None,
'profile_picture_file' : request.files 'profile_picture_file': request.files
} }
return jsonify(data=data) return jsonify(data=data)
return '{}' return '{}'
@app.route('/dashboard/constants') @app.route('/dashboard/constants')
@login_required @login_required
@require_role(roles=['Admin']) @require_role(roles=['Admin'])
def constants(): def constants():
return render_template('/dashboard/constants.html') return render_template('/dashboard/constants.html')
@app.route('/dashboard/rbac') @app.route('/dashboard/rbac')
@login_required @login_required
def rbac(): def rbac():
return render_template('/dashboard/rbac.html') return render_template('/dashboard/rbac.html')

View File

@@ -1,14 +1,17 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, RadioField, FileField from wtforms import StringField, PasswordField, BooleanField, SubmitField, RadioField, FileField
from wtforms.validators import ValidationError, DataRequired, EqualTo, Email, URL from wtforms.validators import ValidationError, DataRequired, EqualTo, Email, URL
from app.models import User from app.models import User
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()]) username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me') remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign in') submit = SubmitField('Sign in')
class RegistrationForm(FlaskForm): class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()]) username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()]) email = StringField('Email', validators=[DataRequired(), Email()])
@@ -20,17 +23,21 @@ class RegistrationForm(FlaskForm):
user = User.query.filter_by(username=username.data).first() user = User.query.filter_by(username=username.data).first()
if user is not None: if user is not None:
raise ValidationError('That username is not available.') raise ValidationError('That username is not available.')
def validate_email(self, email): def validate_email(self, email):
user = User.query.filter_by(email=email.data).first() user = User.query.filter_by(email=email.data).first()
if user is not None: if user is not None:
raise ValidationError('That email address is not available.') raise ValidationError('That email address is not available.')
class ProfileSettingsForm(FlaskForm): class ProfileSettingsForm(FlaskForm):
show_email = RadioField('Show Email', default='registered', choices=[('public', 'Public'), ('registered', 'Registered Users Only'), ('hidden', 'Hidden')]) show_email = RadioField('Show Email', default='registered',
choices=[('public', 'Public'), ('registered', 'Registered Users Only'),
('hidden', 'Hidden')])
submit = SubmitField('Save Profile Settings') submit = SubmitField('Save Profile Settings')
class ProfilePictureForm(FlaskForm): class ProfilePictureForm(FlaskForm):
profile_picture_file = FileField('Upload Profile Picture') profile_picture_file = FileField('Upload Profile Picture')
profile_picture_url = StringField('Use URL for Profile Picture', validators=[URL()]) profile_picture_url = StringField('Use URL for Profile Picture', validators=[URL()])
submit = SubmitField('Submit Profile Picture') submit = SubmitField('Submit Profile Picture')

View File

@@ -1,22 +1,27 @@
from app import app
import flask import flask
from app import app
@app.route('/ftbhot/about') @app.route('/ftbhot/about')
@app.route('/ftbhot/about/') @app.route('/ftbhot/about/')
def ftbhot_about(): def ftbhot_about():
return flask.render_template('/ftbhot/about.html') return flask.render_template('/ftbhot/about.html')
@app.route('/ftbhot/auth') @app.route('/ftbhot/auth')
@app.route('/ftbhot/auth/') @app.route('/ftbhot/auth/')
def ftbhot_auth(): def ftbhot_auth():
return 'WIP' return 'WIP'
@app.route('/ftbhot') @app.route('/ftbhot')
@app.route('/ftbhot/') @app.route('/ftbhot/')
def ftbhot(): def ftbhot():
return flask.render_template('/ftbhot/embed.html') return flask.render_template('/ftbhot/embed.html')
@app.route('/ftbhot/json') @app.route('/ftbhot/json')
@app.route('/ftbhot/json/') @app.route('/ftbhot/json/')
def ftbhot_embed(): def ftbhot_embed():
return flask.render_template('/ftbhot/current.json') return flask.render_template('/ftbhot/current.json')

View File

@@ -1,13 +1,16 @@
from app import app, db, login
from app.models import Search
from app.custom import require_role
from flask_login import current_user, login_user, logout_user, login_required
from flask import request, render_template
import requests
import xmltodict
import base64 import base64
import json import json
import requests
import xmltodict
from flask import request, render_template
from flask_login import current_user, login_required
from app import app, db
from app.custom import require_role
from app.models import Search
@app.route('/hidden/history') @app.route('/hidden/history')
@login_required @login_required
@require_role(roles=['Hidden', 'Admin']) @require_role(roles=['Hidden', 'Admin'])
@@ -21,6 +24,7 @@ def hidden_history():
def hidden_help(): def hidden_help():
return render_template('hidden_help.html') return render_template('hidden_help.html')
# Parses strings to test for "boolean-ness" # Parses strings to test for "boolean-ness"
def boolparse(string, default=False): def boolparse(string, default=False):
trues = ['true', '1'] trues = ['true', '1']
@@ -30,6 +34,7 @@ def boolparse(string, default=False):
return True return True
return False return False
@app.route('/hidden/') @app.route('/hidden/')
@login_required @login_required
@require_role(roles=['Hidden']) @require_role(roles=['Hidden'])
@@ -39,7 +44,8 @@ def hidden():
try: try:
page = int(request.args.get('page') or 1) page = int(request.args.get('page') or 1)
except (TypeError, ValueError): except (TypeError, ValueError):
return '\"page\" parameter must be Integer.<br>Invalid \"page\" parameter: \"{}\"'.format(request.args.get('page')) return '\"page\" parameter must be Integer.<br>Invalid \"page\" parameter: \"{}\"'.format(
request.args.get('page'))
# Handled within building # Handled within building
try: try:
count = int(request.args.get('count') or 50) count = int(request.args.get('count') or 50)
@@ -50,7 +56,7 @@ def hidden():
showfull = boolparse(request.args.get('showfull')) showfull = boolparse(request.args.get('showfull'))
showtags = boolparse(request.args.get('showtags')) showtags = boolparse(request.args.get('showtags'))
# Request, Parse & Build Data # Request, Parse & Build Data
data = build_data(tags, page-1, count, base64, showfull) data = build_data(tags, page - 1, count, base64, showfull)
# Handling for limiters # Handling for limiters
if base64: if base64:
if showfull: if showfull:
@@ -60,14 +66,18 @@ def hidden():
search = Search(user_id=current_user.id, exact_url=str(request.url), query_args=json.dumps(request.args.to_dict())) search = Search(user_id=current_user.id, exact_url=str(request.url), query_args=json.dumps(request.args.to_dict()))
db.session.add(search) db.session.add(search)
db.session.commit() db.session.commit()
return render_template('hidden.html', title='Gelbooru Browser', data=data, tags=tags, page=page, count=count, base64=base64, showfull=showfull, showtags=showtags) return render_template('hidden.html', title='Gelbooru Browser', data=data, tags=tags, page=page, count=count,
base64=base64, showfull=showfull, showtags=showtags)
def base64ify(url): def base64ify(url):
return base64.b64encode(requests.get(url).content).decode() return base64.b64encode(requests.get(url).content).decode()
gelbooru_api_url = "https://gelbooru.com/index.php?page=dapi&s=post&q=index&tags={}&pid={}&limit={}" gelbooru_api_url = "https://gelbooru.com/index.php?page=dapi&s=post&q=index&tags={}&pid={}&limit={}"
gelbooru_view_url = "https://gelbooru.com/index.php?page=post&s=view&id={}" gelbooru_view_url = "https://gelbooru.com/index.php?page=post&s=view&id={}"
def build_data(tags, page, count, base64, showfull): def build_data(tags, page, count, base64, showfull):
# URL Building & Request # URL Building & Request
temp = gelbooru_api_url.format(tags, page, count) temp = gelbooru_api_url.format(tags, page, count)
@@ -75,7 +85,7 @@ def build_data(tags, page, count, base64, showfull):
# XML Parsing & Data Building # XML Parsing & Data Building
parse = xmltodict.parse(response) parse = xmltodict.parse(response)
build = [] build = []
try: try:
parse['posts']['post'] parse['posts']['post']
except KeyError: except KeyError:
@@ -83,13 +93,13 @@ def build_data(tags, page, count, base64, showfull):
for index, element in enumerate(parse['posts']['post'][:count]): for index, element in enumerate(parse['posts']['post'][:count]):
temp = { temp = {
'index' : str(index + 1), 'index': str(index + 1),
'real_url' : element['@file_url'], 'real_url': element['@file_url'],
'sample_url' : element['@preview_url'], 'sample_url': element['@preview_url'],
# strips tags, ensures no empty tags (may be unnecessary) # strips tags, ensures no empty tags (may be unnecessary)
'tags' : list(filter(lambda tag : tag != '', [tag.strip() for tag in element['@tags'].split(' ')])), 'tags': list(filter(lambda tag: tag != '', [tag.strip() for tag in element['@tags'].split(' ')])),
'view' : gelbooru_view_url.format(element['@id']) 'view': gelbooru_view_url.format(element['@id'])
} }
if base64: if base64:
if not showfull: if not showfull:
temp['base64'] = base64ify(temp['sample_url']) temp['base64'] = base64ify(temp['sample_url'])
@@ -97,4 +107,4 @@ def build_data(tags, page, count, base64, showfull):
temp['base64'] = base64ify(temp['real_url']) temp['base64'] = base64ify(temp['real_url'])
build.append(temp) build.append(temp)
return build return build

View File

@@ -1,9 +1,11 @@
from flask import abort
from flask_login import UserMixin
from datetime import datetime from datetime import datetime
from app import db, login
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login
# Just a note, my role system is really quite terrible, but I've implemented as good as a system as I can for a simple RBAC without Hierarchy. # Just a note, my role system is really quite terrible, but I've implemented as good as a system as I can for a simple RBAC without Hierarchy.
# Once could create a complex system, but it would be better to properly work with SQLAlchemy to create proper permissions, hierarchy, parent/child etc. rather than to work with simple strings. # Once could create a complex system, but it would be better to properly work with SQLAlchemy to create proper permissions, hierarchy, parent/child etc. rather than to work with simple strings.
# One should look into perhaps Pickled Python objects if they were interested in simplifying interactions while opening a lot more data storage. # One should look into perhaps Pickled Python objects if they were interested in simplifying interactions while opening a lot more data storage.
@@ -20,7 +22,7 @@ class User(UserMixin, db.Model):
about_me = db.Column(db.String(320)) about_me = db.Column(db.String(320))
last_seen = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow)
show_email = db.Column(db.Boolean, default=False) show_email = db.Column(db.Boolean, default=False)
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)
@@ -28,7 +30,7 @@ class User(UserMixin, db.Model):
if self.password_hash is None: if self.password_hash is None:
raise "{} has no password_hash set!".format(self.__repr__()) raise "{} has no password_hash set!".format(self.__repr__())
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
# Retains order while making sure that there are no duplicate role values and they are capitalized # Retains order while making sure that there are no duplicate role values and they are capitalized
def post_role_processing(self): def post_role_processing(self):
user_roles = self.get_roles() user_roles = self.get_roles()
@@ -51,7 +53,7 @@ class User(UserMixin, db.Model):
raise e raise e
success = False success = False
return success return success
def get_roles(self): def get_roles(self):
return self.uroles.split(' ') return self.uroles.split(' ')
@@ -70,7 +72,7 @@ class User(UserMixin, db.Model):
self.uroles = user_roles self.uroles = user_roles
if postprocess: if postprocess:
self.post_role_processing() self.post_role_processing()
def has_role(self, role): def has_role(self, role):
return self.has_roles([role]) return self.has_roles([role])
@@ -92,6 +94,7 @@ class User(UserMixin, db.Model):
def __repr__(self): def __repr__(self):
return '<User {}>'.format(self.username) return '<User {}>'.format(self.username)
class Search(db.Model): class Search(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
exact_url = db.Column(db.String(160)) exact_url = db.Column(db.String(160))
@@ -102,6 +105,7 @@ class Search(db.Model):
def __repr__(self): def __repr__(self):
return '<Search by {} @ {}>'.format(User.query.filter_by(id=self.user_id).first().username, self.timestamp) return '<Search by {} @ {}>'.format(User.query.filter_by(id=self.user_id).first().username, self.timestamp)
class Post(db.Model): class Post(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140)) body = db.Column(db.String(140))
@@ -111,6 +115,7 @@ class Post(db.Model):
def __repr__(self): def __repr__(self):
return '<Post {}>'.format(self.body) return '<Post {}>'.format(self.body)
@login.user_loader @login.user_loader
def load_user(id): def load_user(id):
return User.query.get(int(id)) return User.query.get(int(id))

View File

@@ -1,8 +1,11 @@
from app import app
from textwrap import wrap
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO from io import BytesIO
from textwrap import wrap
import flask import flask
from PIL import Image, ImageDraw, ImageFont
from app import app
@app.route('/panzer/') @app.route('/panzer/')
@app.route('/panzer') @app.route('/panzer')
@@ -14,6 +17,7 @@ def panzer(string='bionicles are cooler than sex'):
image = create_panzer(string) image = create_panzer(string)
return serve_pil_image(image) return serve_pil_image(image)
def create_panzer(string): def create_panzer(string):
img = Image.open("./app/static/panzer.jpeg") img = Image.open("./app/static/panzer.jpeg")
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
@@ -27,8 +31,9 @@ def create_panzer(string):
draw.text((topleft[0], topleft[1] + (y * 33)), text, font=font2) draw.text((topleft[0], topleft[1] + (y * 33)), text, font=font2)
return img return img
def serve_pil_image(pil_img): def serve_pil_image(pil_img):
img_io = BytesIO() img_io = BytesIO()
pil_img.save(img_io, 'JPEG', quality=50) pil_img.save(img_io, 'JPEG', quality=50)
img_io.seek(0) img_io.seek(0)
return flask.send_file(img_io, mimetype='image/jpeg') return flask.send_file(img_io, mimetype='image/jpeg')

View File

@@ -1,35 +1,37 @@
from app import app, db, login
from app.models import User, Search
from app.forms import LoginForm, RegistrationForm
from app.custom import require_role
from werkzeug.urls import url_parse
from flask import render_template, redirect, url_for, flash, request, jsonify, abort, send_file
from flask_login import current_user, login_user, logout_user, login_required
from multiprocessing import Value
import flask
import requests
import xmltodict
import random
import string
import faker
import json import json
import pprint import pprint
import os import random
import sys import string
import faker
import requests
from flask import render_template, redirect, url_for, flash, request, jsonify
from flask_login import current_user, login_user, logout_user, login_required
from werkzeug.urls import url_parse
from app import app, db
from app.custom import require_role
from app.forms import LoginForm, RegistrationForm
from app.models import User
print = pprint.PrettyPrinter().pprint print = pprint.PrettyPrinter().pprint
fake = faker.Faker() fake = faker.Faker()
strgen = lambda length, charset=string.ascii_letters, weights=None : ''.join(random.choices(list(charset), k=length, weights=weights)) strgen = lambda length, charset=string.ascii_letters, weights=None: ''.join(
random.choices(list(charset), k=length, weights=weights))
@app.route('/', subdomain='api') @app.route('/', subdomain='api')
def api_index(): def api_index():
return "api" return "api"
@app.route('/time/') @app.route('/time/')
def time(): def time():
value = request.args.get('value') value = request.args.get('value')
if not value: if not value:
return '<br>'.join(['[int] value', '[int list] lengths', '[string list] strings', '[boolean] reverse', '[string] pluralappend', '[boolean] synonym']) return '<br>'.join(
['[int] value', '[int list] lengths', '[string list] strings', '[boolean] reverse', '[string] pluralappend',
'[boolean] synonym'])
value = int(value) value = int(value)
lengths = request.args.get('lengths') lengths = request.args.get('lengths')
if lengths: lengths = lengths.split(',') if lengths: lengths = lengths.split(',')
@@ -40,9 +42,13 @@ def time():
if lengths: lengths = list(map(int, lengths)) if lengths: lengths = list(map(int, lengths))
reverse = request.args.get('reverse') reverse = request.args.get('reverse')
if reverse: reverse = bool(reverse) if reverse: reverse = bool(reverse)
return timeformat(value=value, lengths=lengths or [60, 60, 24, 365], strings=strings or ['second', 'minute', 'hour', 'day', 'year'], reverse=True if reverse is None else reverse) return timeformat(value=value, lengths=lengths or [60, 60, 24, 365],
strings=strings or ['second', 'minute', 'hour', 'day', 'year'],
reverse=True if reverse is None else reverse)
def timeformat(value, lengths=[60, 60, 24, 365], strings=['second', 'minute', 'hour', 'day', 'year'], reverse=True, pluralappend='s', synonym=False):
def timeformat(value, lengths=[60, 60, 24, 365], strings=['second', 'minute', 'hour', 'day', 'year'], reverse=True,
pluralappend='s', synonym=False):
converted = [value] converted = [value]
for index, length in enumerate(lengths): for index, length in enumerate(lengths):
temp = converted[-1] // length temp = converted[-1] // length
@@ -51,18 +57,20 @@ def timeformat(value, lengths=[60, 60, 24, 365], strings=['second', 'minute', 'h
if temp != 0: if temp != 0:
converted.append(temp) converted.append(temp)
else: else:
break break
strings = strings[:len(converted)] strings = strings[:len(converted)]
build = ['{} {}'.format(value, strings[i] + pluralappend if value > 1 or value == 0 else strings[i]) for i, value in enumerate(converted)][::-1] build = ['{} {}'.format(value, strings[i] + pluralappend if value > 1 or value == 0 else strings[i]) for i, value in
enumerate(converted)][::-1]
build = ', '.join(build) build = ', '.join(build)
return build return build
@app.route('/avatar/') @app.route('/avatar/')
@app.route('/avatar/<id>/') @app.route('/avatar/<id>/')
@app.route('/avatar/<id>') @app.route('/avatar/<id>')
def getAvatar(id=''): def getAvatar(id=''):
# Constants # Constants
headers = {'Authorization' : f'Bot {app.config["DISCORD_TOKEN"]}'} headers = {'Authorization': f'Bot {app.config["DISCORD_TOKEN"]}'}
api = "https://discordapp.com/api/v6/users/{}" api = "https://discordapp.com/api/v6/users/{}"
cdn = "https://cdn.discordapp.com/avatars/{}/{}.png" cdn = "https://cdn.discordapp.com/avatars/{}/{}.png"
# Get User Data which contains Avatar Hash # Get User Data which contains Avatar Hash
@@ -73,23 +81,25 @@ def getAvatar(id=''):
url = cdn.format(id, user['avatar']) url = cdn.format(id, user['avatar'])
return "<img src=\"{}\">".format(url) return "<img src=\"{}\">".format(url)
@app.route('/userinfo/') @app.route('/userinfo/')
@login_required @login_required
@require_role(roles=['Admin']) @require_role(roles=['Admin'])
def user_info(): def user_info():
prepare = { prepare = {
'id' : current_user.get_id(), 'id': current_user.get_id(),
'email' : current_user.email, 'email': current_user.email,
'username' : current_user.username, 'username': current_user.username,
'password_hash' : current_user.password_hash, 'password_hash': current_user.password_hash,
'is_active' : current_user.is_active, 'is_active': current_user.is_active,
'is_anonymous' : current_user.is_anonymous, 'is_anonymous': current_user.is_anonymous,
'is_authenticated' : current_user.is_authenticated, 'is_authenticated': current_user.is_authenticated,
'metadata' : current_user.metadata.info, 'metadata': current_user.metadata.info,
'uroles' : current_user.get_roles() 'uroles': current_user.get_roles()
} }
return jsonify(prepare) return jsonify(prepare)
@app.route('/') @app.route('/')
def index(): def index():
jobs = [ jobs = [
@@ -101,6 +111,7 @@ def index():
] ]
return render_template('index.html', job=random.choice(jobs)) return render_template('index.html', job=random.choice(jobs))
@app.route('/register/', methods=['GET', 'POST']) @app.route('/register/', methods=['GET', 'POST'])
def register(): def register():
if current_user.is_authenticated: if current_user.is_authenticated:
@@ -115,6 +126,7 @@ def register():
return redirect(url_for('login')) return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form, hideRegister=True) return render_template('register.html', title='Register', form=form, hideRegister=True)
@app.route('/login/', methods=['GET', 'POST']) @app.route('/login/', methods=['GET', 'POST'])
def login(): def login():
if current_user.is_authenticated: if current_user.is_authenticated:
@@ -132,7 +144,8 @@ def login():
return redirect(next_page) return redirect(next_page)
return render_template('login.html', title='Login', form=form, hideLogin=True) return render_template('login.html', title='Login', form=form, hideLogin=True)
@app.route('/logout/') @app.route('/logout/')
def logout(): def logout():
logout_user() logout_user()
return redirect(url_for('index')) return redirect(url_for('index'))

View File

@@ -1,27 +1,35 @@
from app import app
from flask import send_from_directory, redirect, url_for, render_template
import mistune
import os import os
import mistune
from flask import send_from_directory, redirect, url_for, render_template
from app import app
markdown = mistune.Markdown() markdown = mistune.Markdown()
@app.route('/keybase.txt') @app.route('/keybase.txt')
def keybase(): def keybase():
return app.send_static_file('keybase.txt') return app.send_static_file('keybase.txt')
@app.route('/modpacks') @app.route('/modpacks')
def modpacks(): def modpacks():
return markdown(open(os.path.join(app.root_path, 'static', 'MODPACKS.MD'), 'r').read()) return markdown(open(os.path.join(app.root_path, 'static', 'MODPACKS.MD'), 'r').read())
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico',
mimetype='image/vnd.microsoft.icon')
@app.errorhandler(401) @app.errorhandler(401)
def unauthorized(e): def unauthorized(e):
return redirect(url_for('login')) return redirect(url_for('login'))
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
# note that we set the 404 status explicitly # note that we set the 404 status explicitly
return render_template('error.html', code=404, message='Content not found...'), 404 return render_template('error.html', code=404, message='Content not found...'), 404

View File

@@ -1,20 +1,16 @@
from app import app, db, limiter from flask import Response, send_file, request, jsonify
from app.sound_models import YouTubeAudio, SoundcloudAudio, CouldNotDecode, CouldNotDownload, CouldNotProcess
from flask import Response, send_file, redirect, url_for, render_template, request, jsonify
from flask_login import current_user from flask_login import current_user
from multiprocessing import Value
from mutagen.mp3 import MP3 from app import app, db, limiter
import os from app.sound_models import YouTubeAudio, CouldNotDecode, CouldNotDownload, CouldNotProcess
import re
import json
import subprocess
# Selection of Lambdas for creating new responses # Selection of Lambdas for creating new responses
# Not sure if Responses change based on Request Context, but it doesn't hurt. # Not sure if Responses change based on Request Context, but it doesn't hurt.
getBadRequest = lambda : Response('Bad request', status=400, mimetype='text/plain') getBadRequest = lambda: Response('Bad request', status=400, mimetype='text/plain')
getNotImplemented = lambda : Response('Not implemented', status=501, mimetype='text/plain') getNotImplemented = lambda: Response('Not implemented', status=501, mimetype='text/plain')
getInvalidID = lambda : Response('Invalid ID', status=400, mimetype='text/plain') getInvalidID = lambda: Response('Invalid ID', status=400, mimetype='text/plain')
getNotDownloaded = lambda : Response('Media not yet downloaded', status=400, mimetype='text/plain') getNotDownloaded = lambda: Response('Media not yet downloaded', status=400, mimetype='text/plain')
# Retrieves the YouTubeAudio object relevant to the mediaid if available. If not, it facilitates the creation and writing of one. # Retrieves the YouTubeAudio object relevant to the mediaid if available. If not, it facilitates the creation and writing of one.
# Also helps with access times. # Also helps with access times.
@@ -22,22 +18,24 @@ def get_youtube(mediaid):
audio = YouTubeAudio.query.get(mediaid) audio = YouTubeAudio.query.get(mediaid)
if audio is not None: if audio is not None:
audio.access() audio.access()
return audio # sets the access time to now return audio # sets the access time to now
else: else:
audio = YouTubeAudio(id=mediaid) audio = YouTubeAudio(id=mediaid)
audio.fill_metadata() audio.fill_metadata()
audio.download() audio.download()
# Commit and save new audio object into the database # Commit and save new audio object into the database
db.session.add(audio) db.session.add(audio)
db.session.commit() db.session.commit()
return audio return audio
basic_responses = { basic_responses = {
CouldNotDecode : 'Could not decode process response.', CouldNotDecode: 'Could not decode process response.',
CouldNotDownload : 'Could not download video.', CouldNotDownload: 'Could not download video.',
CouldNotProcess : 'Could not process.' CouldNotProcess: 'Could not process.'
} }
# A simple function among the routes to determine what should be returned. # A simple function among the routes to determine what should be returned.
# Not particularly sure how request context is passed, but it seems that either it passed or can access current_user's authenitcation/role's properly, so no problem. # Not particularly sure how request context is passed, but it seems that either it passed or can access current_user's authenitcation/role's properly, so no problem.
# Shows error in full context IF authenticated + admin, otherwise basic error description, OTHERWISE a basic error message. # Shows error in full context IF authenticated + admin, otherwise basic error description, OTHERWISE a basic error message.
@@ -48,7 +46,8 @@ def errorCheck(e):
raise e raise e
if current_user.is_authenticated and current_user.has_role('Admin'): response = str(e) + '\n' + response if current_user.is_authenticated and current_user.has_role('Admin'): response = str(e) + '\n' + response
return Response(response, status=200, mimetype='text/plain') return Response(response, status=200, mimetype='text/plain')
# Under the request context, it grabs the same args needed to decide whether the stream has been downloaded previously # Under the request context, it grabs the same args needed to decide whether the stream has been downloaded previously
# It applies rate limiting differently based on service, and whether the stream has been accessed previously # It applies rate limiting differently based on service, and whether the stream has been accessed previously
def downloadLimiter(): def downloadLimiter():
@@ -60,9 +59,10 @@ def downloadLimiter():
else: else:
return '10/minute' return '10/minute'
# Streams back the specified media back to the client # Streams back the specified media back to the client
@app.route('/stream/<service>/<mediaid>') @app.route('/stream/<service>/<mediaid>')
@limiter.limit(downloadLimiter, lambda : 'global', error_message='429 Too Many Requests') @limiter.limit(downloadLimiter, lambda: 'global', error_message='429 Too Many Requests')
def stream(service, mediaid): def stream(service, mediaid):
if service == 'youtube': if service == 'youtube':
if YouTubeAudio.isValid(mediaid): if YouTubeAudio.isValid(mediaid):
@@ -80,6 +80,7 @@ def stream(service, mediaid):
else: else:
return getBadRequest() return getBadRequest()
# Returns the duration of a specific media # Returns the duration of a specific media
@app.route('/duration/<service>/<mediaid>') @app.route('/duration/<service>/<mediaid>')
def duration(service, mediaid): def duration(service, mediaid):
@@ -93,6 +94,7 @@ def duration(service, mediaid):
else: else:
return getBadRequest() return getBadRequest()
# Returns a detailed JSON export of a specific database entry. # Returns a detailed JSON export of a specific database entry.
# Will not create a new database entry where one didn't exist before. # Will not create a new database entry where one didn't exist before.
@app.route('/status/<service>/<mediaid>') @app.route('/status/<service>/<mediaid>')
@@ -113,6 +115,7 @@ def status(service, mediaid):
else: else:
return getBadRequest() return getBadRequest()
@app.route('/list/<service>') @app.route('/list/<service>')
def list(service): def list(service):
if service == 'youtube': if service == 'youtube':
@@ -125,6 +128,7 @@ def list(service):
else: else:
return getBadRequest() return getBadRequest()
@app.route('/all/<service>') @app.route('/all/<service>')
def all(service): def all(service):
if service == 'youtube': if service == 'youtube':
@@ -136,4 +140,4 @@ def all(service):
elif service == 'spotify': elif service == 'spotify':
return getNotImplemented() return getNotImplemented()
else: else:
return getBadRequest() return getBadRequest()

View File

@@ -1,9 +1,11 @@
from datetime import datetime
from app import db
import subprocess
import json import json
import os import os
import re import re
import subprocess
from datetime import datetime
from app import db
# Returned when a erroring status code is returned. May end up hitting false positives, where the file was still produced properly # Returned when a erroring status code is returned. May end up hitting false positives, where the file was still produced properly
# yet a erroring status code was returned. May be a good measure to always disconnect when a error code is found. # yet a erroring status code was returned. May be a good measure to always disconnect when a error code is found.
@@ -11,27 +13,31 @@ import re
class CouldNotProcess(Exception): class CouldNotProcess(Exception):
pass pass
# Shouldn't happen in most cases. When a file isn't found, yet the status code for the process returned positive. # Shouldn't happen in most cases. When a file isn't found, yet the status code for the process returned positive.
class CouldNotDownload(Exception): class CouldNotDownload(Exception):
pass pass
# When a JSON returning command returns undecodable JSON # When a JSON returning command returns undecodable JSON
# This shouldn't occur and will only be available when a unforseen error occurs where JSON cannot be read, # This shouldn't occur and will only be available when a unforseen error occurs where JSON cannot be read,
# yet a non-erroring status code was returned! # yet a non-erroring status code was returned!
class CouldNotDecode(Exception): class CouldNotDecode(Exception):
pass pass
# A Database Object describing a Audio File originating from YouTube # A Database Object describing a Audio File originating from YouTube
# Stores basic information like Title/Uploader/URL etc. as well as holds methods useful # Stores basic information like Title/Uploader/URL etc. as well as holds methods useful
# for manipulating, deleting, downloading, updating, and accessing the relevant information or file. # for manipulating, deleting, downloading, updating, and accessing the relevant information or file.
class YouTubeAudio(db.Model): class YouTubeAudio(db.Model):
id = db.Column(db.String(11), primary_key=True) # 11 char id, presumed to stay the same for the long haul. Should be able to change to 12 chars. id = db.Column(db.String(11),
url = db.Column(db.String(64)) # 43 -> 64 primary_key=True) # 11 char id, presumed to stay the same for the long haul. Should be able to change to 12 chars.
title = db.Column(db.String(128)) # 120 > 128 url = db.Column(db.String(64)) # 43 -> 64
creator = db.Column(db.String(128)) # Seems to be Uploader set, so be careful with this title = db.Column(db.String(128)) # 120 > 128
uploader = db.Column(db.String(32)) # 20 -> 32 creator = db.Column(db.String(128)) # Seems to be Uploader set, so be careful with this
filename = db.Column(db.String(156)) # 128 + 11 + 1 -> 156 uploader = db.Column(db.String(32)) # 20 -> 32
duration = db.Column(db.Integer) filename = db.Column(db.String(156)) # 128 + 11 + 1 -> 156
duration = db.Column(db.Integer)
access_count = db.Column(db.Integer, default=0) access_count = db.Column(db.Integer, default=0)
download_timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) download_timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
last_access_timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) last_access_timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
@@ -61,20 +67,23 @@ class YouTubeAudio(db.Model):
self.filename = self.id + '.mp3' self.filename = self.id + '.mp3'
command = f'youtube-dl -4 -x --audio-format mp3 --restrict-filenames --dump-json {self.id}' command = f'youtube-dl -4 -x --audio-format mp3 --restrict-filenames --dump-json {self.id}'
process = subprocess.Popen(command.split(' '), process = subprocess.Popen(command.split(' '),
encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
data = process.communicate() data = process.communicate()
if process.returncode != 0: if process.returncode != 0:
raise CouldNotProcess(f'Command: {command}\n{data[1]}Exit Code: {process.returncode}') # process ends with a newline, not needed between raise CouldNotProcess(
f'Command: {command}\n{data[1]}Exit Code: {process.returncode}') # process ends with a newline, not needed between
try: try:
data = json.loads(data[0]) data = json.loads(data[0])
except json.JSONDecodeError: except json.JSONDecodeError:
raise CouldNotDecode(data) # We'll return the process data, figure out what to do with it higher up in stack (return/diagnose etc.) raise CouldNotDecode(
data) # We'll return the process data, figure out what to do with it higher up in stack (return/diagnose etc.)
print(f'JSON acquired for {self.id}, beginning to fill.') print(f'JSON acquired for {self.id}, beginning to fill.')
self.duration = data['duration'] self.duration = data['duration']
self.url = data['webpage_url'] # Could be created, but we'll just infer from JSON response self.url = data['webpage_url'] # Could be created, but we'll just infer from JSON response
self.creator = data['creator'] or data['uploader'] self.creator = data['creator'] or data['uploader']
self.uploader = data['uploader'] or data['creator'] self.uploader = data['uploader'] or data['creator']
self.title = data['title'] or data['alt_title'] # Do not trust alt-title ; it is volatile and uploader set, e.x. https://i.imgur.com/Tgff4rI.png self.title = data['title'] or data[
'alt_title'] # Do not trust alt-title ; it is volatile and uploader set, e.x. https://i.imgur.com/Tgff4rI.png
print(f'Metadata filled for {self.id}') print(f'Metadata filled for {self.id}')
db.session.commit() db.session.commit()
@@ -83,26 +92,27 @@ class YouTubeAudio(db.Model):
print(f'Attempting download of {self.id}') print(f'Attempting download of {self.id}')
command = f'youtube-dl -x -4 --restrict-filenames --audio-quality 64K --audio-format mp3 -o ./app/sounds/youtube/%(id)s.%(ext)s {self.id}' command = f'youtube-dl -x -4 --restrict-filenames --audio-quality 64K --audio-format mp3 -o ./app/sounds/youtube/%(id)s.%(ext)s {self.id}'
process = subprocess.Popen(command.split(' '), encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) process = subprocess.Popen(command.split(' '), encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
data = process.communicate() # Not the data for the mp3, just the output. We have to separate this in order to 'wait' for the process to complete fully. data = process.communicate() # Not the data for the mp3, just the output. We have to separate this in order to 'wait' for the process to complete fully.
print('Checking process return code...') print('Checking process return code...')
if process.returncode != 0: if process.returncode != 0:
raise CouldNotProcess(f'Command: {command}\n{data[1] or data[0]}Exit Code: {process.returncode}') raise CouldNotProcess(f'Command: {command}\n{data[1] or data[0]}Exit Code: {process.returncode}')
print('Checking for expected file...') print('Checking for expected file...')
if not os.path.exists(self.getPath()): if not os.path.exists(self.getPath()):
raise CouldNotDownload(data[1] or data[0]) raise CouldNotDownload(data[1] or data[0])
print(f'Download attempt for {self.id} finished successfully.') print(f'Download attempt for {self.id} finished successfully.')
# Validates whether the specified ID could be a valid YouTube video ID
# Validates whether the specified ID could be a valid YouTube video ID
@staticmethod @staticmethod
def isValid(id): def isValid(id):
return re.match(r'^[A-Za-z0-9_-]{11}$', id) is not None return re.match(r'^[A-Za-z0-9_-]{11}$', id) is not None
# Returns a JSON serialization of the database entry # Returns a JSON serialization of the database entry
def toJSON(self, noConvert=False): def toJSON(self, noConvert=False):
data = {'id' : self.id, 'url' : self.url, 'title' : self.title, 'creator' : self.creator, data = {'id': self.id, 'url': self.url, 'title': self.title, 'creator': self.creator,
'uploader' : self.uploader, 'filename' : self.filename, 'duration' : self.duration, 'uploader': self.uploader, 'filename': self.filename, 'duration': self.duration,
'access_count' : self.access_count, 'download_timestamp' : self.download_timestamp.isoformat(), 'access_count': self.access_count, 'download_timestamp': self.download_timestamp.isoformat(),
'last_access_timestamp' : self.last_access_timestamp.isoformat()} 'last_access_timestamp': self.last_access_timestamp.isoformat()}
return data if noConvert else json.dumps(data) return data if noConvert else json.dumps(data)
def delete(self): def delete(self):
@@ -114,10 +124,11 @@ class YouTubeAudio(db.Model):
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
class SoundcloudAudio(db.Model): class SoundcloudAudio(db.Model):
id = db.Column(db.Integer, primary_key=True) # hidden API-accessible only ID id = db.Column(db.Integer, primary_key=True) # hidden API-accessible only ID
url = db.Column(db.String(256)) url = db.Column(db.String(256))
title = db.Column(db.String(128)) title = db.Column(db.String(128))
creator = db.Column(db.String(64)) creator = db.Column(db.String(64))
filename = db.Column(db.String(156)) filename = db.Column(db.String(156))
duration = db.Column(db.Integer) duration = db.Column(db.Integer)

View File

@@ -1,22 +1,23 @@
import json
import os
import time
from flask import send_file
from app import app from app import app
from config import Config from config import Config
from flask import send_from_directory, redirect, url_for, render_template, send_file
import json
import subprocess
import time
import os
from .spotify_explicit import main from .spotify_explicit import main
path = os.path.join('app/spotify_explicit/recent.json') path = os.path.join('app/spotify_explicit/recent.json')
def check_and_update(): def check_and_update():
try: try:
with open(path) as file: with open(path) as file:
file = json.load(file) file = json.load(file)
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
file = {'last_generated' : -1} file = {'last_generated': -1}
if file['last_generated'] == -1: if file['last_generated'] == -1:
return True return True
else: else:
@@ -28,12 +29,13 @@ def check_and_update():
ideal = file['last_generated'] + Config.SPOTIFY_CACHE_TIME ideal = file['last_generated'] + Config.SPOTIFY_CACHE_TIME
# print(f'Waiting another {int(ideal - time.time())} seconds') # print(f'Waiting another {int(ideal - time.time())} seconds')
return False return False
@app.route('/spotify/') @app.route('/spotify/')
def spotify(): def spotify():
if check_and_update(): if check_and_update():
print('Graph out of date - running update command') print('Graph out of date - running update command')
with open(path, 'w+') as file: with open(path, 'w+') as file:
file = json.dump({'last_generated' : int(time.time())}, file) file = json.dump({'last_generated': int(time.time())}, file)
main.main() main.main()
return send_file('spotify_explicit/export/export.png') return send_file('spotify_explicit/export/export.png')

View File

@@ -1,4 +1,7 @@
import logging, sys, os, json import json
import logging
import os
import sys
# Path to API Credentials file # Path to API Credentials file
PATH = os.path.join(sys.path[0], 'auth.json') PATH = os.path.join(sys.path[0], 'auth.json')
@@ -9,11 +12,11 @@ if not os.path.exists(PATH):
# Dump a pretty-printed dictionary with default values # Dump a pretty-printed dictionary with default values
json.dump( json.dump(
{ {
'USERNAME' : 'Your Username Here', 'USERNAME': 'Your Username Here',
'CLIENT_ID' : 'Your Client ID Here', 'CLIENT_ID': 'Your Client ID Here',
'CLIENT_SECRET' : 'Your Client Secret Here', 'CLIENT_SECRET': 'Your Client Secret Here',
'REDIRECT_URI' : 'Your Redirect URI Callback Here', 'REDIRECT_URI': 'Your Redirect URI Callback Here',
'SCOPE' : ['Your Scopes Here'] 'SCOPE': ['Your Scopes Here']
}, },
file, file,
indent=3 indent=3
@@ -31,4 +34,4 @@ USERNAME = FILE['USERNAME']
CLIENT_ID = FILE['CLIENT_ID'] CLIENT_ID = FILE['CLIENT_ID']
CLIENT_SECRET = FILE['CLIENT_SECRET'] CLIENT_SECRET = FILE['CLIENT_SECRET']
REDIRECT_URI = FILE['REDIRECT_URI'] REDIRECT_URI = FILE['REDIRECT_URI']
SCOPE = ' '.join(FILE['SCOPE']) SCOPE = ' '.join(FILE['SCOPE'])

View File

@@ -1,11 +1,12 @@
import json
import logging
import os import os
import sys import sys
import time import time
import json
from . import auth from . import auth
from . import pull
from . import process from . import process
import logging from . import pull
def main(): def main():
@@ -14,6 +15,7 @@ def main():
refresh() refresh()
process.main() process.main()
# Refreshes tracks from files if the token from Spotipy has expired, # Refreshes tracks from files if the token from Spotipy has expired,
# thus keeping us up to date in most cases while keeping rate limits # thus keeping us up to date in most cases while keeping rate limits
def refresh(): def refresh():
@@ -28,5 +30,6 @@ def refresh():
else: else:
pull.main() pull.main()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,13 +1,11 @@
import os
import sys
import json import json
import logging import logging
import datetime import os
import collections
import numpy as np
import dateutil.parser import dateutil.parser
import PIL.Image as Image
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np
# Gets all files in tracks folder, returns them in parsed JSON # Gets all files in tracks folder, returns them in parsed JSON
def get_files(): def get_files():
@@ -20,6 +18,7 @@ def get_files():
) )
return files return files
# Simple function to combine a bunch of items from different files # Simple function to combine a bunch of items from different files
def combine_files(files): def combine_files(files):
items = [] items = []
@@ -27,6 +26,7 @@ def combine_files(files):
items.extend(file['items']) items.extend(file['items'])
return items return items
# Prints the data in a interesting format # Prints the data in a interesting format
def print_data(data): def print_data(data):
for i, item in enumerate(data): for i, item in enumerate(data):
@@ -36,6 +36,7 @@ def print_data(data):
artists = ' & '.join(artist['name'] for artist in item['track']['artists']) artists = ' & '.join(artist['name'] for artist in item['track']['artists'])
print('[{}] {} "{}" by {}'.format(date, explicit, track_name, artists)) print('[{}] {} "{}" by {}'.format(date, explicit, track_name, artists))
def process_data(data): def process_data(data):
# Process the data by Month/Year, then by Clean/Explicit # Process the data by Month/Year, then by Clean/Explicit
scores = {} scores = {}
@@ -44,7 +45,7 @@ def process_data(data):
if date not in scores.keys(): if date not in scores.keys():
scores[date] = [0, 0] scores[date] = [0, 0]
scores[date][1 if item['track']['explicit'] else 0] += 1 scores[date][1 if item['track']['explicit'] else 0] += 1
# Create simplified arrays for each piece of data # Create simplified arrays for each piece of data
months = list(scores.keys())[::-1] months = list(scores.keys())[::-1]
clean, explicit = [], [] clean, explicit = [], []
@@ -59,17 +60,17 @@ def process_data(data):
ind = np.arange(n) ind = np.arange(n)
width = 0.55 width = 0.55
# Resizer figuresize to be 2.0 wider # Resizer figuresize to be 2.0 wider
plt.figure(figsize=(10.0, 6.0)) plt.figure(figsize=(10.0, 6.0))
# Stacked Bars # Stacked Bars
p1 = plt.bar(ind, explicit, width) p1 = plt.bar(ind, explicit, width)
p2 = plt.bar(ind, clean, width, bottom=explicit) # bottom= just has the bar sit on top of the explicit p2 = plt.bar(ind, clean, width, bottom=explicit) # bottom= just has the bar sit on top of the explicit
# Plot labeling # Plot labeling
plt.title('Song Count by Clean/Explicit') plt.title('Song Count by Clean/Explicit')
plt.ylabel('Song Count') plt.ylabel('Song Count')
plt.xlabel('Month') plt.xlabel('Month')
plt.xticks(ind, months, rotation=270) # Rotation 90 will have the plt.xticks(ind, months, rotation=270) # Rotation 90 will have the
plt.legend((p1[0], p2[0]), ('Explicit', 'Clean')) plt.legend((p1[0], p2[0]), ('Explicit', 'Clean'))
fig = plt.gcf() # Magic to save to image and then show fig = plt.gcf() # Magic to save to image and then show
# Save the figure, overwriting anything in your way # Save the figure, overwriting anything in your way
logging.info('Saving the figure to the \'export\' folder') logging.info('Saving the figure to the \'export\' folder')
@@ -86,7 +87,7 @@ def process_data(data):
dpi=100, dpi=100,
quality=95 quality=95
) )
# Finally show the figure to # Finally show the figure to
logging.info('Showing plot to User') logging.info('Showing plot to User')
# plt.show() # plt.show()
@@ -95,6 +96,7 @@ def process_data(data):
# logging.info('Copying the plot data to clipboard') # logging.info('Copying the plot data to clipboard')
# copy(months, clean, explicit) # copy(months, clean, explicit)
# Simple method for exporting data to a table like format # Simple method for exporting data to a table like format
# Will paste into Excel very easily # Will paste into Excel very easily
def copy(months, clean, explicit): def copy(months, clean, explicit):
@@ -104,6 +106,7 @@ def copy(months, clean, explicit):
f'{item[0]}\t{item[1]}\t{item[2]}' for item in zip(months, clean, explicit) f'{item[0]}\t{item[1]}\t{item[2]}' for item in zip(months, clean, explicit)
])) ]))
def main(): def main():
# logging.basicConfig(level=logging.INFO) # logging.basicConfig(level=logging.INFO)
logging.info("Reading track files") logging.info("Reading track files")
@@ -111,7 +114,7 @@ def main():
logging.info(f"Read and parse {len(files)} track files") logging.info(f"Read and parse {len(files)} track files")
logging.info("Combining into single track file for ease of access") logging.info("Combining into single track file for ease of access")
data = combine_files(files) data = combine_files(files)
data.sort(key=lambda item : dateutil.parser.parse(item['added_at']).timestamp(), reverse=True) data.sort(key=lambda item: dateutil.parser.parse(item['added_at']).timestamp(), reverse=True)
logging.info(f'File combined with {len(data)} items') logging.info(f'File combined with {len(data)} items')
logging.info('Processing file...') logging.info('Processing file...')
process_data(data) process_data(data)

View File

@@ -1,13 +1,14 @@
import os
import sys
from . import auth
import json import json
import shutil
import pprint
import spotipy
import logging import logging
from hurry.filesize import size, alternative import os
import shutil
import spotipy
import spotipy.util as util import spotipy.util as util
from hurry.filesize import size, alternative
from . import auth
def main(): def main():
# Get Authorization # Get Authorization
@@ -27,8 +28,8 @@ def main():
tracks_folder = os.path.join(os.path.dirname(__file__), 'tracks') tracks_folder = os.path.join(os.path.dirname(__file__), 'tracks')
logging.warning('Clearing all files in tracks folder for new files') logging.warning('Clearing all files in tracks folder for new files')
if os.path.exists(tracks_folder): if os.path.exists(tracks_folder):
shutil.rmtree(tracks_folder) # Delete folder and all contents (old track files) shutil.rmtree(tracks_folder) # Delete folder and all contents (old track files)
os.makedirs(tracks_folder) # Recreate the folder just deleted os.makedirs(tracks_folder) # Recreate the folder just deleted
logging.info('Cleared folder, ready to download new track files') logging.info('Cleared folder, ready to download new track files')
curoffset, curlimit = 0, 50 curoffset, curlimit = 0, 50
@@ -62,4 +63,4 @@ def main():
)) ))
break break
# Continuing, so increment offset # Continuing, so increment offset
curoffset += curlimit curoffset += curlimit