Moved old webpage, app & client into .archive folder
This commit is contained in:
108
.archive/app/.gitignore
vendored
Normal file
108
.archive/app/.gitignore
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
.static_storage/
|
||||
.media/
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# - - - - -
|
||||
# My own gitignore files and folders
|
||||
env_variables.py
|
||||
224
.archive/app/classedStray.py
Executable file
224
.archive/app/classedStray.py
Executable file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-05 18:40:11
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2018-04-03 22:58:20
|
||||
import os.path, hashlib, time, glob, sqlite3, re, json, tweepy
|
||||
import logging
|
||||
from functools import reduce
|
||||
from fuzzywuzzy import process
|
||||
from langdetect import detect
|
||||
from time import sleep
|
||||
import env_variables as env
|
||||
|
||||
dirHash = None
|
||||
|
||||
class twitter(object):
|
||||
def __init__(self):
|
||||
if '' in [env.consumer_key, env.consumer_secret, env.access_token, env.access_token_secret]:
|
||||
logging.warning('Twitter api keys not set!')
|
||||
|
||||
self.consumer_key = env.consumer_key
|
||||
self.consumer_secret = env.consumer_secret
|
||||
self.access_token = env.access_token
|
||||
self.access_token_secret = env.access_token_secret
|
||||
|
||||
self.authenticate()
|
||||
|
||||
def authenticate(self):
|
||||
auth = tweepy.OAuthHandler(self.consumer_key, self.consumer_secret)
|
||||
auth.set_access_token(self.access_token, self.access_token_secret)
|
||||
self.api_token = tweepy.API(auth)
|
||||
|
||||
def api(self):
|
||||
return self.api_token
|
||||
|
||||
def dm(self, message, user='kevinmidboe'):
|
||||
response = self.api_token.send_direct_message(user, text=message)
|
||||
|
||||
class strayEpisode(object):
|
||||
def __init__(self, parent, childrenList):
|
||||
self.parent = parent
|
||||
self.children = childrenList
|
||||
self._id = self.getUniqueID()
|
||||
self.showName = self.findSeriesName()
|
||||
self.season = self.getSeasonNumber()
|
||||
self.episode = self.getEpisodeNumber()
|
||||
self.videoFiles = []
|
||||
self.subtitles = []
|
||||
self.trash = []
|
||||
self.sortMediaItems()
|
||||
|
||||
if self.saveToDB():
|
||||
self.notifyInsert()
|
||||
|
||||
def getUniqueID(self):
|
||||
# conn = sqlite3.connect(env.db_path)
|
||||
# c = conn.cursor()
|
||||
|
||||
# c.execute("SELECT id FROM stray_eps WHERE id is " + )
|
||||
return hashlib.md5("b'{}'".format(self.parent).encode()).hexdigest()[:8]
|
||||
|
||||
def findSeriesName(self):
|
||||
find = re.compile("^[a-zA-Z0-9. ]*")
|
||||
m = re.match(find, self.parent)
|
||||
if m:
|
||||
name, hit = process.extractOne(m.group(0), getShowNames().keys())
|
||||
if hit >= 60:
|
||||
return name
|
||||
else:
|
||||
# This should be logged or handled somehow
|
||||
return 'Unmatched!'
|
||||
|
||||
def getSeasonNumber(self):
|
||||
m = re.search('[sS][0-9]{1,2}', self.parent)
|
||||
if m:
|
||||
return re.sub('[sS]', '', m.group(0))
|
||||
|
||||
def getEpisodeNumber(self):
|
||||
m = re.search('[eE][0-9]{1,2}', self.parent)
|
||||
if m:
|
||||
return re.sub('[eE]', '', m.group(0))
|
||||
|
||||
def removeUploadSign(self, file):
|
||||
match = re.search('-[a-zA-Z\[\]\-]*.[a-z]{3}', file)
|
||||
if match:
|
||||
uploader = match.group(0)[:-4]
|
||||
return re.sub(uploader, '', file)
|
||||
|
||||
return file
|
||||
|
||||
def analyseSubtitles(self, subFile):
|
||||
# TODO verify that it is a file
|
||||
try:
|
||||
subtitlePath = os.path.join([env.input_dir, self.parent, subFile])
|
||||
except TypeError:
|
||||
# TODO don't get a list in subtitlePath
|
||||
return self.removeUploadSign(subFile)
|
||||
f = open(subtitlesPath, 'r', encoding='ISO-8859-15')
|
||||
language = detect(f.read())
|
||||
f.close()
|
||||
|
||||
file = self.removeUploadSign(subFile)
|
||||
if 'sdh' in subFile.lower():
|
||||
return '.'.join([file[:-4], 'sdh', language, file[-3:]])
|
||||
|
||||
return '.'.join([file[:-4], language, file[-3:]])
|
||||
|
||||
def sortMediaItems(self):
|
||||
for child in self.children:
|
||||
if child[-3:] in env.mediaExt and child[:-4] not in env.mediaExcluders:
|
||||
self.videoFiles.append([child, self.removeUploadSign(child)])
|
||||
elif child[-3:] in env.subExt:
|
||||
self.subtitles.append([child, self.analyseSubtitles(child)])
|
||||
else:
|
||||
self.trash.append(child)
|
||||
|
||||
def notifyInsert(self):
|
||||
# Send unique id. (time)
|
||||
tweetObj = twitter()
|
||||
if self.showName is None:
|
||||
message = 'Error adding ep: ' + self._id
|
||||
else:
|
||||
message = 'Added episode:\n' + self.showName + ' S' + self.season\
|
||||
+ 'E' + self.episode + '\nDetails: \n https://kevinmidboe.com/seasoned/verified.html?id=' + self._id
|
||||
tweetObj.dm(message)
|
||||
|
||||
|
||||
def saveToDB(self):
|
||||
# TODO Setup script
|
||||
conn = sqlite3.connect(env.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
path = '/'.join([env.input_dir, self.parent])
|
||||
video_files = json.dumps(self.videoFiles)
|
||||
subtitles = json.dumps(self.subtitles)
|
||||
trash = json.dumps(self.trash)
|
||||
|
||||
try:
|
||||
c.execute("INSERT INTO stray_eps ('id', 'parent', 'path', 'name', 'season', 'episode', 'video_files', 'subtitles', 'trash') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", \
|
||||
[self._id, self.parent, path, self.showName, self.season, self.episode, video_files, subtitles, trash])
|
||||
except sqlite3.IntegrityError:
|
||||
logging.info(self._id + ': episode already registered')
|
||||
return False
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def getDirContent(dir=env.input_dir):
|
||||
# TODO What if item in db is not in this list?
|
||||
try:
|
||||
return [d for d in os.listdir(dir) if d[0] != '.']
|
||||
except FileNotFoundError:
|
||||
# TODO Log to error file
|
||||
logging.info('Error: "' + dir + '" is not a directory.')
|
||||
|
||||
# Hashes the contents of media folder to easily check for changes.
|
||||
def directoryChecksum():
|
||||
dirList = getDirContent()
|
||||
# Creates a string of all the list items.
|
||||
dirConcat = reduce(lambda x, y: x + y, dirList, "")
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(bytes(dirConcat, 'utf-16be')) # String to byte conversion.
|
||||
global dirHash
|
||||
if dirHash != m.digest():
|
||||
dirHash = m.digest()
|
||||
return True
|
||||
return False
|
||||
|
||||
def getShowNames():
|
||||
conn = sqlite3.connect(env.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT show_names, date_added, date_modified FROM shows')
|
||||
|
||||
returnList = {}
|
||||
for name, added, modified in c.fetchall():
|
||||
returnList[name] = [added, modified]
|
||||
|
||||
conn.close()
|
||||
return returnList
|
||||
|
||||
def XOR(list1, list2):
|
||||
return set(list1) ^ set(list2)
|
||||
|
||||
def filterChildItems(parent):
|
||||
try:
|
||||
children = getDirContent('/'.join([env.input_dir, parent]))
|
||||
if children:
|
||||
strayEpisode(parent, children)
|
||||
except FileNotFoundError:
|
||||
# TODO Log to error file
|
||||
logging.info('Error: "' + '/'.join([env.input_dir, parent]) + '" is not a valid directory.')
|
||||
|
||||
def getNewItems():
|
||||
newItems = XOR(getDirContent(), getShowNames())
|
||||
for item in newItems:
|
||||
filterChildItems(item)
|
||||
|
||||
|
||||
def main():
|
||||
# TODO Verify env variables (showDir)
|
||||
start_time = time.time()
|
||||
if directoryChecksum():
|
||||
getNewItems()
|
||||
|
||||
logging.debug("--- %s seconds ---" % '{0:.4f}'.format((time.time() - start_time)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if (os.path.exists(env.logfile)):
|
||||
logging.basicConfig(filename=env.logfile, level=logging.DEBUG)
|
||||
else:
|
||||
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
|
||||
exit(0)
|
||||
|
||||
while True:
|
||||
main()
|
||||
sleep(30)
|
||||
|
||||
279
.archive/app/core.py
Executable file
279
.archive/app/core.py
Executable file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-08-25 23:22:27
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-10-12 22:44:27
|
||||
|
||||
from guessit import guessit
|
||||
import os, errno
|
||||
import logging
|
||||
import tvdb_api
|
||||
from pprint import pprint
|
||||
|
||||
import env_variables as env
|
||||
|
||||
from video import VIDEO_EXTENSIONS, Episode, Movie, Video
|
||||
from subtitle import SUBTITLE_EXTENSIONS, Subtitle, get_subtitle_path
|
||||
from utils import sanitize
|
||||
|
||||
logging.basicConfig(filename=os.path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
#: Supported archive extensions
|
||||
ARCHIVE_EXTENSIONS = ('.rar',)
|
||||
|
||||
def scan_video(path):
|
||||
"""Scan a video from a `path`.
|
||||
|
||||
:param str path: existing path to the video.
|
||||
:return: the scanned video.
|
||||
:rtype: :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check video extension
|
||||
# if not path.endswith(VIDEO_EXTENSIONS):
|
||||
# raise ValueError('%r is not a valid video extension' % os.path.splitext(path)[1])
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
logging.info('Scanning video %r in %r', filename, dirpath)
|
||||
|
||||
# guess
|
||||
parent_path = path.strip(filename)
|
||||
# video = Video.fromguess(filename, parent_path, guessit(path))
|
||||
video = Video(filename)
|
||||
# guessit(path)
|
||||
|
||||
return video
|
||||
|
||||
|
||||
def scan_subtitle(path):
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
logging.info('Scanning subtitle %r in %r', filename, dirpath)
|
||||
|
||||
# guess
|
||||
parent_path = path.strip(filename)
|
||||
subtitle = Subtitle.fromguess(filename, parent_path, guessit(path))
|
||||
|
||||
|
||||
return subtitle
|
||||
|
||||
|
||||
def scan_files(path, age=None, archives=True):
|
||||
"""Scan `path` for videos and their subtitles.
|
||||
|
||||
See :func:`refine` to find additional information for the video.
|
||||
|
||||
:param str path: existing directory path to scan.
|
||||
:param datetime.timedelta age: maximum age of the video or archive.
|
||||
:param bool archives: scan videos in archives.
|
||||
:return: the scanned videos.
|
||||
:rtype: list of :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check for non-directory path
|
||||
if not os.path.isdir(path):
|
||||
raise ValueError('Path is not a directory')
|
||||
|
||||
name_dict = {}
|
||||
|
||||
# walk the path
|
||||
mediafiles = []
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
logging.debug('Walking directory %r', dirpath)
|
||||
|
||||
# remove badly encoded and hidden dirnames
|
||||
for dirname in list(dirnames):
|
||||
if dirname.startswith('.'):
|
||||
logging.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
|
||||
dirnames.remove(dirname)
|
||||
|
||||
# scan for videos
|
||||
for filename in filenames:
|
||||
# filter on videos and archives
|
||||
if not (filename.endswith(VIDEO_EXTENSIONS) or filename.endswith(SUBTITLE_EXTENSIONS) or archives and filename.endswith(ARCHIVE_EXTENSIONS)):
|
||||
continue
|
||||
|
||||
# skip hidden files
|
||||
if filename.startswith('.'):
|
||||
logging.debug('Skipping hidden filename %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# reconstruct the file path
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
|
||||
# skip links
|
||||
if os.path.islink(filepath):
|
||||
logging.debug('Skipping link %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# skip old files
|
||||
if age and datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(filepath)) > age:
|
||||
logging.debug('Skipping old file %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# scan
|
||||
if filename.endswith(VIDEO_EXTENSIONS): # video
|
||||
try:
|
||||
video = scan_video(filepath)
|
||||
try:
|
||||
name_dict[video.series] += 1
|
||||
except KeyError:
|
||||
name_dict[video.series] = 0
|
||||
mediafiles.append(video)
|
||||
|
||||
except ValueError: # pragma: no cover
|
||||
logging.exception('Error scanning video')
|
||||
continue
|
||||
elif archives and filename.endswith(ARCHIVE_EXTENSIONS): # archive
|
||||
print('archive')
|
||||
pass
|
||||
# try:
|
||||
# video = scan_archive(filepath)
|
||||
# mediafiles.append(video)
|
||||
# except (NotRarFile, RarCannotExec, ValueError): # pragma: no cover
|
||||
# logging.exception('Error scanning archive')
|
||||
# continue
|
||||
elif filename.endswith(SUBTITLE_EXTENSIONS): # subtitle
|
||||
try:
|
||||
subtitle = scan_subtitle(filepath)
|
||||
mediafiles.append(subtitle)
|
||||
except ValueError:
|
||||
logging.exception('Error scanning subtitle')
|
||||
continue
|
||||
else: # pragma: no cover
|
||||
raise ValueError('Unsupported file %r' % filename)
|
||||
|
||||
|
||||
pprint(name_dict)
|
||||
return mediafiles
|
||||
|
||||
|
||||
def organize_files(path):
|
||||
hashList = {}
|
||||
mediafiles = scan_files(path)
|
||||
# print(mediafiles)
|
||||
|
||||
for file in mediafiles:
|
||||
hashList.setdefault(file.__hash__(),[]).append(file)
|
||||
# hashList[file.__hash__()] = file
|
||||
|
||||
return hashList
|
||||
|
||||
|
||||
def save_subtitles(files, single=False, directory=None, encoding=None):
|
||||
t = tvdb_api.Tvdb()
|
||||
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
for file in files:
|
||||
# TODO this should not be done in the loop
|
||||
dirname = "%s S%sE%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode))
|
||||
|
||||
createParentfolder = not dirname in file.parent_path
|
||||
if createParentfolder:
|
||||
dirname = os.path.join(file.parent_path, dirname)
|
||||
print('Created: %s' % dirname)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
# TODO Clean this !
|
||||
try:
|
||||
tvdb_episode = t[file.series][file.season][file.episode]
|
||||
episode_title = tvdb_episode['episodename']
|
||||
except:
|
||||
episode_title = ''
|
||||
|
||||
old = os.path.join(file.parent_path, file.name)
|
||||
|
||||
if file.name.endswith(SUBTITLE_EXTENSIONS):
|
||||
lang = file.getLanguage()
|
||||
sdh = '.sdh' if file.sdh else ''
|
||||
filename = "%s S%sE%s %s%s.%s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, sdh, lang, file.container)
|
||||
else:
|
||||
filename = "%s S%sE%s %s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, file.container)
|
||||
|
||||
if createParentfolder:
|
||||
newname = os.path.join(dirname, filename)
|
||||
else:
|
||||
newname = os.path.join(file.parent_path, filename)
|
||||
|
||||
|
||||
print('Moved: %s ---> %s' % (old, newname))
|
||||
os.rename(old, newname)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
# for hash in files:
|
||||
# hashIndex = [files[hash]]
|
||||
# for hashItems in hashIndex:
|
||||
# for file in hashItems:
|
||||
# print(file.series)
|
||||
|
||||
# saved_subtitles = []
|
||||
# for subtitle in files:
|
||||
# # check content
|
||||
# if subtitle.name is None:
|
||||
# logging.error('Skipping subtitle %r: no content', subtitle)
|
||||
# continue
|
||||
|
||||
# # check language
|
||||
# if subtitle.language in set(s.language for s in saved_subtitles):
|
||||
# logging.debug('Skipping subtitle %r: language already saved', subtitle)
|
||||
# continue
|
||||
|
||||
# # create subtitle path
|
||||
# subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
|
||||
# if directory is not None:
|
||||
# subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
|
||||
|
||||
# # save content as is or in the specified encoding
|
||||
# logging.info('Saving %r to %r', subtitle, subtitle_path)
|
||||
# if encoding is None:
|
||||
# with io.open(subtitle_path, 'wb') as f:
|
||||
# f.write(subtitle.content)
|
||||
# else:
|
||||
# with io.open(subtitle_path, 'w', encoding=encoding) as f:
|
||||
# f.write(subtitle.text)
|
||||
# saved_subtitles.append(subtitle)
|
||||
|
||||
# # check single
|
||||
# if single:
|
||||
# break
|
||||
|
||||
# return saved_subtitles
|
||||
|
||||
def stringTime():
|
||||
return str(datetime.now().strftime("%Y-%m-%d %H:%M:%S:%f"))
|
||||
|
||||
|
||||
def main():
|
||||
# episodePath = '/Volumes/media/tv/Black Mirror/Black Mirror Season 01/'
|
||||
episodePath = '/Volumes/mainframe/shows/Black Mirror/Black Mirror Season 01/'
|
||||
|
||||
t = tvdb_api.Tvdb()
|
||||
|
||||
hashList = organize_files(episodePath)
|
||||
pprint(hashList)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
99
.archive/app/magnet.py
Executable file
99
.archive/app/magnet.py
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python
|
||||
'''
|
||||
Created on Apr 19, 2012
|
||||
@author: dan, Faless
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE - Version 3
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
'''
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
import os.path as pt
|
||||
import sys, logging
|
||||
import libtorrent as lt
|
||||
from time import sleep
|
||||
|
||||
import env_variables as env
|
||||
|
||||
logging.basicConfig(filename=pt.dirname(__file__) + '/' + env.logfile)
|
||||
|
||||
def magnet2torrent(magnet, output_name=None):
|
||||
if output_name and \
|
||||
not pt.isdir(output_name) and \
|
||||
not pt.isdir(pt.dirname(pt.abspath(output_name))):
|
||||
logging.info("Invalid output folder: " + pt.dirname(pt.abspath(output_name)))
|
||||
logging.info("")
|
||||
sys.exit(0)
|
||||
|
||||
tempdir = tempfile.mkdtemp()
|
||||
ses = lt.session()
|
||||
params = {
|
||||
'save_path': tempdir,
|
||||
'storage_mode': lt.storage_mode_t(2),
|
||||
'paused': False,
|
||||
'auto_managed': True,
|
||||
'duplicate_is_error': True
|
||||
}
|
||||
handle = lt.add_magnet_uri(ses, magnet, params)
|
||||
|
||||
logging.info("Downloading Metadata (this may take a while)")
|
||||
while (not handle.has_metadata()):
|
||||
try:
|
||||
sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Aborting...")
|
||||
ses.pause()
|
||||
logging.info("Cleanup dir " + tempdir)
|
||||
shutil.rmtree(tempdir)
|
||||
sys.exit(0)
|
||||
ses.pause()
|
||||
logging.info("Done")
|
||||
|
||||
torinfo = handle.get_torrent_info()
|
||||
torfile = lt.create_torrent(torinfo)
|
||||
|
||||
output = pt.abspath(torinfo.name() + ".torrent")
|
||||
|
||||
if output_name:
|
||||
if pt.isdir(output_name):
|
||||
output = pt.abspath(pt.join(
|
||||
output_name, torinfo.name() + ".torrent"))
|
||||
elif pt.isdir(pt.dirname(pt.abspath(output_name))):
|
||||
output = pt.abspath(output_name)
|
||||
|
||||
logging.info("Saving torrent file here : " + output + " ...")
|
||||
torcontent = lt.bencode(torfile.generate())
|
||||
f = open(output, "wb")
|
||||
f.write(lt.bencode(torfile.generate()))
|
||||
f.close()
|
||||
logging.info("Saved! Cleaning up dir: " + tempdir)
|
||||
ses.remove_torrent(handle)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
return output
|
||||
|
||||
def main():
|
||||
magnet = sys.argv[1]
|
||||
logging.info('INPUT: {}'.format(magnet))
|
||||
|
||||
magnet2torrent(magnet, env.torrent_dumpsite)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
.archive/app/modules/addSubLanguage.py
Executable file
26
.archive/app/modules/addSubLanguage.py
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-04 13:46:28
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-04 14:03:57
|
||||
|
||||
from langdetect import detect
|
||||
from removeUploader import removeUploader
|
||||
|
||||
testFiles = ['subs/The.Man.from.U.N.C.L.E.2015.1080p-[eztv].srt',
|
||||
'subs/The.Man.from.U.N.C.L.E.2015.1080p-[eztv]ENGLUISH.srt']
|
||||
|
||||
def detectLanguage(file):
|
||||
f = open(file, 'r', encoding= 'ISO-8859-15')
|
||||
language = detect(f.read())
|
||||
f.close()
|
||||
|
||||
return removeUploader(file)[:-3] + language + '.srt'
|
||||
|
||||
def addLangExtension():
|
||||
for file in testFiles:
|
||||
print(detectLanguage(file))
|
||||
|
||||
if __name__ == '__main__':
|
||||
addLangExtension()
|
||||
16
.archive/app/modules/createPasteee.py
Executable file
16
.archive/app/modules/createPasteee.py
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-02-23 21:41:40
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-05 19:35:10
|
||||
|
||||
from pasteee import Paste
|
||||
|
||||
def createPasteee():
|
||||
paste = Paste('Test pastee', views=10)
|
||||
print(paste)
|
||||
print(paste['raw'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
createPasteee()
|
||||
23
.archive/app/modules/dirHash.py
Executable file
23
.archive/app/modules/dirHash.py
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-05 15:24:17
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-04-05 18:22:13
|
||||
import os, hashlib
|
||||
from functools import reduce
|
||||
|
||||
hashDir = '/Volumes/media/tv'
|
||||
|
||||
def main():
|
||||
dirList = os.listdir(hashDir)
|
||||
concat = reduce(lambda x, y: x + y, dirList, "")
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(bytes(concat, 'utf-16be'))
|
||||
return m.digest()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(main())
|
||||
|
||||
# TODO The hash value should be saved in a global manner
|
||||
125
.archive/app/modules/folderCreator.py
Executable file
125
.archive/app/modules/folderCreator.py
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-05 13:52:45
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-05 17:14:25
|
||||
|
||||
import sqlite3, json, os, tweepy
|
||||
from re import sub
|
||||
|
||||
dbPath = 'shows.db'
|
||||
|
||||
consumer_key, consumer_secret = 'yvVTrxNtVsLkoHxKWxh4xvgjg', '39OW6Q8fIKDXvTPPCaEJDULcYaHC5XZ3fe7HHCGtdepBKui2jK'
|
||||
access_token, access_token_secret = '3214835117-OXVVLYeqUScRAPMqfVw5hS8NI63zPnWOVK63C5I', 'ClcGnF8vW6DbvgRgjwU6YjDc9f2oxMzOvUAN8kzpsmbcL'
|
||||
|
||||
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
|
||||
auth.set_access_token(access_token, access_token_secret)
|
||||
api = tweepy.API(auth)
|
||||
|
||||
def newnameMediaitems(media_items):
|
||||
# media_items = [['New.Girl.S06E18.720p.HDTV.x264-EZTV.srt', '-EZTV', 'nl'], ['New.Girl.S06E18.720p.HDTV.x264-FLEET.srt', '-FLEET', 'en']]
|
||||
media_items = json.loads(media_items)
|
||||
|
||||
returnList = []
|
||||
for item in media_items:
|
||||
returnList.append([item[0], item[0].replace(item[1], '')])
|
||||
|
||||
return returnList
|
||||
|
||||
def newnameSubtitles(subtitles):
|
||||
subtitles = json.loads(subtitles)
|
||||
|
||||
returnList = []
|
||||
for item in subtitles:
|
||||
returnList.append([item[0], item[0].replace(item[1], '.' + item[2])])
|
||||
|
||||
return returnList
|
||||
|
||||
|
||||
def updateMovedStatus(episodeDict):
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('UPDATE stray_episodes SET moved = 1 WHERE original is "' + episodeDict['original'] + '"')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
def unpackEpisodes():
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
cursor = c.execute('SELECT * FROM stray_episodes WHERE verified = 1 AND moved = 0')
|
||||
episodeList = []
|
||||
for row in c.fetchall():
|
||||
columnNames = [description[0] for description in cursor.description]
|
||||
|
||||
episodeDict = dict.fromkeys(columnNames)
|
||||
|
||||
for i, key in enumerate(episodeDict.keys()):
|
||||
episodeDict[key] = row[i]
|
||||
|
||||
episodeList.append(episodeDict)
|
||||
|
||||
conn.close()
|
||||
|
||||
return episodeList
|
||||
|
||||
def createFolders(episode):
|
||||
showDir = '/media/hdd1/tv/%s/'% episode['name']
|
||||
episodeFormat = '%s S%sE%s/'% (episode['name'], episode['season'], episode['episode'])
|
||||
seasonFormat = '%s Season %s/'% (episode['name'], episode['season'])
|
||||
|
||||
if not os.path.isdir(showDir + seasonFormat):
|
||||
os.makedirs(showDir + seasonFormat)
|
||||
|
||||
if not os.path.isdir(showDir + seasonFormat + episodeFormat):
|
||||
os.makedirs(showDir + seasonFormat + episodeFormat)
|
||||
|
||||
def moveFiles(episode):
|
||||
# TODO All this should be imported from config file
|
||||
showDir = '/media/hdd1/tv/'
|
||||
episodeFormat = '%s S%sE%s/'% (episode['name'], episode['season'], episode['episode'])
|
||||
seasonFormat = '%s/%s Season %s/'% (episode['name'], episode['name'], episode['season'])
|
||||
|
||||
# TODO All this is pretty ballsy to do this hard/stict.
|
||||
newMediaitems = newnameMediaitems(episode['media_items'])
|
||||
for item in newMediaitems:
|
||||
old_location = showDir + episode['original'] + '/' + item[0]
|
||||
new_location = showDir + seasonFormat + episodeFormat + item[1]
|
||||
os.rename(old_location, new_location)
|
||||
|
||||
if episode['subtitles']:
|
||||
newSubtitles = newnameSubtitles(episode['subtitles'])
|
||||
for item in newSubtitles:
|
||||
old_location = showDir + episode['original'] + '/' + item[0]
|
||||
new_location = showDir + seasonFormat + episodeFormat + item[1]
|
||||
os.rename(old_location, new_location)
|
||||
|
||||
# shutil.rmtree(showDir + episode['original'])
|
||||
if episode['trash']:
|
||||
for trash in json.loads(episode['trash']):
|
||||
os.remove(showDir + episode['original'] + '/'+ trash)
|
||||
|
||||
# TODO Maybe move to delete folder instead, than user can dump.
|
||||
os.rmdir(showDir + episode['original'])
|
||||
|
||||
updateMovedStatus(episode)
|
||||
|
||||
api.create_favorite(episode['response_id'])
|
||||
|
||||
|
||||
def findVerified():
|
||||
episodes = unpackEpisodes()
|
||||
if episodes:
|
||||
for episode in episodes:
|
||||
createFolders(episode)
|
||||
moveFiles(episode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
findVerified()
|
||||
|
||||
57
.archive/app/modules/pasteee.py
Normal file
57
.archive/app/modules/pasteee.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python2
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
pasteee module
|
||||
Allows pasting to https://paste.ee
|
||||
https://github.com/i-ghost/pasteee
|
||||
"""
|
||||
|
||||
# 2 <-> 3
|
||||
from urllib.request import urlopen
|
||||
from urllib.request import Request as urlrequest
|
||||
from urllib.parse import urlencode
|
||||
from urllib import error as urlerror
|
||||
import json
|
||||
|
||||
|
||||
class PasteError(Exception):
|
||||
"""Exception class for this module"""
|
||||
pass
|
||||
|
||||
|
||||
class Paste(object):
|
||||
def __new__(cls, paste,
|
||||
private=True, lang="plain",
|
||||
key="public", desc="",
|
||||
expire=0, views=0, encrypted=False):
|
||||
if not paste:
|
||||
raise PasteError("No paste provided")
|
||||
if expire and views:
|
||||
# API incorrectly returns success so we raise error locally
|
||||
raise PasteError("Options 'expire' and 'views' are mutually exclusive")
|
||||
request = urlrequest(
|
||||
"http://paste.ee/api",
|
||||
data=urlencode(
|
||||
{
|
||||
'paste': paste,
|
||||
'private': bool(private),
|
||||
'language': lang,
|
||||
'key': key,
|
||||
'description': desc,
|
||||
'expire': expire,
|
||||
'views': views,
|
||||
'encrypted': bool(encrypted),
|
||||
'format': "json"
|
||||
}
|
||||
).encode("utf-8"),
|
||||
headers={'User-Agent': 'Mozilla/5.0'}
|
||||
)
|
||||
try:
|
||||
result = json.loads(urlopen(request).read().decode("utf-8"))
|
||||
return result["paste"]
|
||||
except urlerror.HTTPError:
|
||||
print("Couldn't send paste")
|
||||
raise
|
||||
except KeyError:
|
||||
raise PasteError("Invalid paste option: %s" % (result["error"]))
|
||||
30
.archive/app/modules/readDB.py
Executable file
30
.archive/app/modules/readDB.py
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-03 22:35:38
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-04 11:09:09
|
||||
|
||||
import sqlite3
|
||||
from fuzzywuzzy import process
|
||||
|
||||
path = "/Users/KevinMidboe/Dropbox/python/seasonedShows/shows.db"
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(path)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT show_names, date_added, date_modified FROM shows')
|
||||
|
||||
returnList = {}
|
||||
for name, added, modified in c.fetchall():
|
||||
returnList[name] = [added, modified]
|
||||
|
||||
while True:
|
||||
query = input('Query: ')
|
||||
print(process.extractOne(query, returnList.keys()))
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
main()
|
||||
24
.archive/app/modules/removeUploader.py
Executable file
24
.archive/app/modules/removeUploader.py
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-04 13:47:32
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-04 13:53:12
|
||||
|
||||
import re
|
||||
testFile = '/Volumes/media/tv/New Girl/New Girl Season 06/New Girl S06E18/New.Girl.S06E18.Young.Adult.1080p.WEB-DL.DD5.1.H264-[eztv]-horse.mkv'
|
||||
|
||||
def removeUploader(file=testFile):
|
||||
match = re.search('-[a-zA-Z\[\]\-]*.[a-z]{3}', file)
|
||||
|
||||
if match and input('Remove uploader:\t' + match.group(0)[:-4] + ' [Y/n] ') != 'n':
|
||||
uploader, ext = match.group(0).split('.')
|
||||
# if ext not in subExtensions:
|
||||
# file = file.replace(uploader, '')
|
||||
# else:
|
||||
# file = file.replace(uploader, '.eng')
|
||||
file = file.replace(uploader, '')
|
||||
return file
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(removeUploader())
|
||||
22
.archive/app/modules/seasonedFolders.py
Executable file
22
.archive/app/modules/seasonedFolders.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-05 15:55:16
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-05 17:25:50
|
||||
|
||||
from time import sleep
|
||||
|
||||
from findStray import findStray
|
||||
from tweetNewEpisodes import tweetNewEpisodes
|
||||
from folderCreator import findVerified
|
||||
|
||||
def main():
|
||||
while True:
|
||||
findStray()
|
||||
tweetNewEpisodes()
|
||||
findVerified()
|
||||
sleep(10)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
12
.archive/app/modules/subLangfinder.py
Executable file
12
.archive/app/modules/subLangfinder.py
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from langdetect import detect
|
||||
|
||||
def main():
|
||||
f = open('/Volumes/media/movies/The Man from UNCLE (2015)/The.Man.from.U.N.C.L.E.2015.1080p.nl.srt', 'r', encoding = "ISO-8859-15")
|
||||
print(detect(f.read()))
|
||||
f.close()
|
||||
print(f.close())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
5180
.archive/app/modules/subs/The.Man.from.U.N.C.L.E.2015.1080p-[eztv].srt
Executable file
5180
.archive/app/modules/subs/The.Man.from.U.N.C.L.E.2015.1080p-[eztv].srt
Executable file
File diff suppressed because it is too large
Load Diff
4815
.archive/app/modules/subs/The.Man.from.U.N.C.L.E.2015.1080p-[eztv]ENGLUISH.srt
Executable file
4815
.archive/app/modules/subs/The.Man.from.U.N.C.L.E.2015.1080p-[eztv]ENGLUISH.srt
Executable file
File diff suppressed because it is too large
Load Diff
137
.archive/app/modules/tweetNewEpisodes.py
Executable file
137
.archive/app/modules/tweetNewEpisodes.py
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-04 16:50:09
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-05 16:51:35
|
||||
|
||||
import tweepy, sqlite3
|
||||
from pasteee import Paste
|
||||
from pprint import pprint
|
||||
|
||||
dbPath = "shows.db"
|
||||
|
||||
consumer_key, consumer_secret = 'yvVTrxNtVsLkoHxKWxh4xvgjg', '39OW6Q8fIKDXvTPPCaEJDULcYaHC5XZ3fe7HHCGtdepBKui2jK'
|
||||
access_token, access_token_secret = '3214835117-OXVVLYeqUScRAPMqfVw5hS8NI63zPnWOVK63C5I', 'ClcGnF8vW6DbvgRgjwU6YjDc9f2oxMzOvUAN8kzpsmbcL'
|
||||
|
||||
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
|
||||
auth.set_access_token(access_token, access_token_secret)
|
||||
api = tweepy.API(auth)
|
||||
|
||||
|
||||
def unpackEpisode(episode):
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
cursor = c.execute('SELECT * FROM stray_episodes')
|
||||
columnNames = [description[0] for description in cursor.description]
|
||||
|
||||
conn.close()
|
||||
|
||||
episodeDict = dict.fromkeys(columnNames)
|
||||
|
||||
for i, key in enumerate(episodeDict.keys()):
|
||||
episodeDict[key] = episode[i]
|
||||
|
||||
return episodeDict
|
||||
|
||||
|
||||
def prettifyEpisode(episode):
|
||||
returnString = ''
|
||||
for key, value in episode.items():
|
||||
returnString += key + ': ' + str(value) + '\n'
|
||||
|
||||
return returnString
|
||||
|
||||
def createPasteee(episode):
|
||||
return Paste(prettifyEpisode(episode), private=False, desc="My first paste", views=10)
|
||||
|
||||
def tweetString(episode):
|
||||
print(type(episode['episode']), episode)
|
||||
tweetString = '@KevinMidboe\nAdded episode:\n' + episode['name'] + ' S' + str(episode['season'])\
|
||||
+ 'E' + str(episode['episode']) + '\nDetails: '
|
||||
return tweetString
|
||||
|
||||
# TODO What if error when tweeting, no id_str
|
||||
def tweet(tweetString):
|
||||
response = api.update_status(status=tweetString)
|
||||
return response.id_str
|
||||
|
||||
|
||||
def updateTweetID(episodeDict, id):
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('UPDATE stray_episodes SET tweet_id = ' + id + ' WHERE original is "' + episodeDict['original'] + '"')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def tweetEpisode(episode):
|
||||
pasteee = createPasteee(episode)
|
||||
tweet_id = tweet(tweetString(episode) + pasteee['raw'])
|
||||
updateTweetID(episode, tweet_id)
|
||||
|
||||
|
||||
|
||||
def getLastTweets(user, count=1):
|
||||
return api.user_timeline(screen_name=user,count=count)
|
||||
|
||||
def verifyByID(id, reponse_id):
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('UPDATE stray_episodes SET verified = 1, response_id = ' + reponse_id + ' WHERE tweet_id is "' + id + '"')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# TODO Add more parsing than confirm
|
||||
def parseReply(tweet):
|
||||
if b'\xf0\x9f\x91\x8d' in tweet.text.encode('utf-8'):
|
||||
print('Verified!')
|
||||
verifyByID(tweet.in_reply_to_status_id_str, tweet.id_str)
|
||||
|
||||
def getReply(tweet):
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
tweetID = tweet.in_reply_to_status_id_str
|
||||
c.execute('SELECT * FROM stray_episodes WHERE tweet_id is ' + tweetID + ' AND verified is 0')
|
||||
row = c.fetchone()
|
||||
|
||||
if row:
|
||||
episode = unpackEpisode(row)
|
||||
conn.close()
|
||||
|
||||
parseReply(tweet)
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
def lookForNewEpisodes():
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT * FROM stray_episodes WHERE tweet_id is NULL')
|
||||
|
||||
for row in c.fetchall():
|
||||
episode = unpackEpisode(row)
|
||||
tweetEpisode(episode)
|
||||
|
||||
conn.close()
|
||||
|
||||
def checkForReply():
|
||||
for tweet in getLastTweets('KevinMidboe', 10):
|
||||
if tweet.in_reply_to_status_id_str != None:
|
||||
getReply(tweet)
|
||||
|
||||
|
||||
def tweetNewEpisodes():
|
||||
lookForNewEpisodes()
|
||||
checkForReply()
|
||||
|
||||
if __name__ == '__main__':
|
||||
tweetNewEpisodes()
|
||||
|
||||
104
.archive/app/modules/twitterConversation.py
Executable file
104
.archive/app/modules/twitterConversation.py
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-03-04 14:06:53
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-03-05 11:48:47
|
||||
|
||||
import tweepy, sqlite3
|
||||
from pasteee import Paste
|
||||
from pprint import pprint
|
||||
|
||||
dbPath = "shows.db"
|
||||
|
||||
consumer_key, consumer_secret = 'yvVTrxNtVsLkoHxKWxh4xvgjg', '39OW6Q8fIKDXvTPPCaEJDULcYaHC5XZ3fe7HHCGtdepBKui2jK'
|
||||
access_token, access_token_secret = '3214835117-OXVVLYeqUScRAPMqfVw5hS8NI63zPnWOVK63C5I', 'ClcGnF8vW6DbvgRgjwU6YjDc9f2oxMzOvUAN8kzpsmbcL'
|
||||
|
||||
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
|
||||
auth.set_access_token(access_token, access_token_secret)
|
||||
api = tweepy.API(auth)
|
||||
apiUser = api.me()
|
||||
apiUsername, apiUserID = apiUser.screen_name, apiUser.id_str
|
||||
|
||||
|
||||
def tweetNewEpisode(episode):
|
||||
createPasteee()
|
||||
|
||||
def unpackEpisode(episode):
|
||||
episodeDict = dict.fromkeys(['original', 'full_path', 'name', 'season', 'episode',\
|
||||
'media_items', 'subtitles', 'trash', 'tweet_id', 'verified'])
|
||||
|
||||
for i, key in enumerate(episodeDict.keys()):
|
||||
episodeDict[key] = episode[i]
|
||||
|
||||
return episodeDict
|
||||
|
||||
def
|
||||
|
||||
def updateTweetID(episodeDict, id):
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('UPDATE stray_episodes SET tweet_id = ' + id + ' WHERE original is ' + episodeDict['original'])
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def getUntweetedEpisodes():
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT * FROM stray_episodes WHERE tweet_id is NULL')
|
||||
|
||||
for episode in c.fetchall():
|
||||
tweetNewEpisode(episode)
|
||||
|
||||
conn.close()
|
||||
exit(0)
|
||||
return episode
|
||||
|
||||
|
||||
|
||||
|
||||
def lastTweet(user, count=1):
|
||||
return api.user_timeline(screen_name=user,count=count)
|
||||
|
||||
def checkReply():
|
||||
originalTweet = lastTweet('pi_midboe')[0]
|
||||
originalID, lastText = originalTweet.id_str, originalTweet.text
|
||||
|
||||
tweets = lastTweet('KevinMidboe', 10)
|
||||
|
||||
for tweet in tweets:
|
||||
tweetID = tweet.in_reply_to_status_id_str
|
||||
if tweetID == originalID:
|
||||
print(tweet.text)
|
||||
|
||||
def unpackEpisodes():
|
||||
conn = sqlite3.connect(dbPath)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT * FROM stray_episodes WHERE tweet_id IS NULL and verified IS 0')
|
||||
|
||||
content = c.fetchall()
|
||||
conn.close()
|
||||
return content
|
||||
|
||||
|
||||
def tweet(tweetString):
|
||||
if not lastTweet('pi_midboe')[0].text.startswith(tweetString):
|
||||
paste = Paste(unpackEpisodes(), private=False, desc="My first paste", views=2)
|
||||
tweetString += paste['raw']
|
||||
response = api.update_status(status=tweetString)
|
||||
print('\n', response.id_str)
|
||||
|
||||
def main():
|
||||
episode = getUntweetedEpisodes()
|
||||
print(episode)
|
||||
tweet('@KevinMidboe\nAdded episode: \nNew Girl S06E16\n\nDetails: ')
|
||||
# unpackEpisodes()
|
||||
checkReply()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
112
.archive/app/moveSeasoned.py
Executable file
112
.archive/app/moveSeasoned.py
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-12 23:27:51
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2018-05-13 19:17:17
|
||||
|
||||
import sys, sqlite3, json, os.path
|
||||
import logging
|
||||
import env_variables as env
|
||||
import shutil
|
||||
|
||||
import delugeClient.deluge_cli as delugeCli
|
||||
|
||||
class episode(object):
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.getVarsFromDB()
|
||||
|
||||
def getVarsFromDB(self):
|
||||
c = sqlite3.connect(env.db_path).cursor()
|
||||
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
|
||||
returnMsg = c.fetchone()
|
||||
self.parent = returnMsg[0]
|
||||
self.name = returnMsg[1]
|
||||
self.season = returnMsg[2]
|
||||
self.episode = returnMsg[3]
|
||||
self.video_files = json.loads(returnMsg[4])
|
||||
self.subtitles = json.loads(returnMsg[5])
|
||||
self.trash = json.loads(returnMsg[6])
|
||||
c.close()
|
||||
|
||||
self.queries = {
|
||||
'parent_input': [env.input_dir, self.parent],
|
||||
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
|
||||
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
|
||||
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
|
||||
}
|
||||
|
||||
def typeDir(self, dType, create=False, mergeItem=None):
|
||||
url = '/'.join(self.queries[dType])
|
||||
print(url)
|
||||
if create and not os.path.isdir(url):
|
||||
os.makedirs(url)
|
||||
fix_ownership(url)
|
||||
if mergeItem:
|
||||
return '/'.join([url, str(mergeItem)])
|
||||
return url
|
||||
|
||||
|
||||
def fix_ownership(path):
|
||||
pass
|
||||
# TODO find this from username from config
|
||||
# uid = 1000
|
||||
# gid = 112
|
||||
# os.chown(path, uid, gid)
|
||||
|
||||
def moveStray(strayId):
|
||||
ep = episode(strayId)
|
||||
|
||||
for item in ep.video_files:
|
||||
try:
|
||||
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
|
||||
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
|
||||
shutil.move(old_dir, new_dir)
|
||||
except FileNotFoundError:
|
||||
logging.warning(old_dir + ' does not exits, cannot be moved.')
|
||||
|
||||
for item in ep.subtitles:
|
||||
try:
|
||||
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
|
||||
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
|
||||
shutil.move(old_dir, new_dir)
|
||||
except FileNotFoundError:
|
||||
logging.warning(old_dir + ' does not exits, cannot be moved.')
|
||||
|
||||
for item in ep.trash:
|
||||
try:
|
||||
os.remove(ep.typeDir('parent_input', mergeItem=item))
|
||||
except FileNotFoundError:
|
||||
logging.warning(ep.typeDir('parent_input', mergeItem=item) + 'does not exist, cannot be removed.')
|
||||
|
||||
fix_ownership(ep.typeDir('episode'))
|
||||
for root, dirs, files in os.walk(ep.typeDir('episode')):
|
||||
for item in files:
|
||||
fix_ownership(os.path.join(ep.typeDir('episode'), item))
|
||||
|
||||
|
||||
# TODO because we might jump over same files, the dir might no longer
|
||||
# be empty and cannot remove dir like this.
|
||||
try:
|
||||
os.rmdir(ep.typeDir('parent_input'))
|
||||
except FileNotFoundError:
|
||||
logging.warning('Cannot remove ' + ep.typeDir('parent_input') + ', file no longer exists.')
|
||||
|
||||
# Remove from deluge client
|
||||
logging.info('Removing {} for deluge'.format(ep.parent))
|
||||
deluge = delugeCli.Deluge()
|
||||
response = deluge.remove(ep.parent)
|
||||
logging.info('Deluge response after delete: {}'.format(response))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
abspath = os.path.abspath(__file__)
|
||||
dname = os.path.dirname(abspath)
|
||||
if (os.path.exists(os.path.join(dname, env.logfile))):
|
||||
logging.basicConfig(filename=env.logfile, level=logging.INFO)
|
||||
else:
|
||||
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
|
||||
|
||||
moveStray(sys.argv[-1])
|
||||
318
.archive/app/pirateSearch.py
Executable file
318
.archive/app/pirateSearch.py
Executable file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-10-12 11:55:03
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-10-22 18:54:18
|
||||
|
||||
import sys, logging, re, json
|
||||
from urllib import parse, request
|
||||
from urllib.error import URLError
|
||||
from bs4 import BeautifulSoup
|
||||
from os import path
|
||||
|
||||
import datetime
|
||||
from pprint import pprint
|
||||
|
||||
from core import stringTime
|
||||
import env_variables as env
|
||||
logging.basicConfig(filename=path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
|
||||
|
||||
RELEASE_TYPES = ('bdremux', 'brremux', 'remux',
|
||||
'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5',
|
||||
'web-cap', 'webcap', 'web cap',
|
||||
'webrip', 'web rip', 'web-rip', 'web',
|
||||
'webdl', 'web dl', 'web-dl', 'hdrip',
|
||||
'dsr', 'dsrip', 'satrip', 'dthrip', 'dvbrip', 'hdtv', 'pdtv', 'tvrip', 'hdtvrip',
|
||||
'dvdr', 'dvd-full', 'full-rip', 'iso',
|
||||
'ts', 'hdts', 'hdts', 'telesync', 'pdvd', 'predvdrip',
|
||||
'camrip', 'cam')
|
||||
|
||||
|
||||
def sanitize(string, ignore_characters=None, replace_characters=None):
|
||||
"""Sanitize a string to strip special characters.
|
||||
|
||||
:param str string: the string to sanitize.
|
||||
:param set ignore_characters: characters to ignore.
|
||||
:return: the sanitized string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# only deal with strings
|
||||
if string is None:
|
||||
return
|
||||
|
||||
replace_characters = replace_characters or ''
|
||||
|
||||
ignore_characters = ignore_characters or set()
|
||||
|
||||
characters = ignore_characters
|
||||
if characters:
|
||||
string = re.sub(r'[%s]' % re.escape(''.join(characters)), replace_characters, string)
|
||||
|
||||
return string
|
||||
|
||||
def return_re_match(string, re_statement):
|
||||
if string is None:
|
||||
return
|
||||
|
||||
m = re.search(re_statement, string)
|
||||
if 'Y-day' in m.group():
|
||||
return datetime.timedelta(days=1).strftime('%m-%d %Y')
|
||||
|
||||
if 'Today' in m.group():
|
||||
return datetime.datetime.now().strftime('%m-%d %Y')
|
||||
return sanitize(m.group(), '\xa0', ' ')
|
||||
|
||||
|
||||
# Can maybe be moved away from this class
|
||||
# returns a number that is either the value of multiple_pages
|
||||
# or if it exceeds total_pages, return total_pages.
|
||||
def pagesToCount(multiple, total):
|
||||
if (multiple > total):
|
||||
return total
|
||||
return multiple
|
||||
|
||||
# Should maybe not be able to set values without checking if they are valid?
|
||||
class piratebay(object):
|
||||
def __init__(self, query=None, page=0, sort=None, category=None):
|
||||
# This should be moved to a config file
|
||||
self.url = 'https://thepiratebay.org/search'
|
||||
self.sortTypes = {
|
||||
'size': 5,
|
||||
'seed_count': 99
|
||||
}
|
||||
self.categoryTypes = {
|
||||
'movies': 207,
|
||||
'porn_movies': 505,
|
||||
}
|
||||
# - - -
|
||||
|
||||
# Req params
|
||||
self.query = query
|
||||
self.page = page
|
||||
self.sort = sort
|
||||
self.category = category
|
||||
self.total_pages = 0
|
||||
self.headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
# self.headers = {}
|
||||
|
||||
def build_URL_request(self):
|
||||
url = '/'.join([self.url, parse.quote(self.query), str(self.page), str(self.sort), str(self.category)])
|
||||
return request.Request(url, headers=self.headers)
|
||||
|
||||
def next_page(self):
|
||||
# If page exceeds the max_page, return None
|
||||
# Can either save the last query/url in the object or have it passed
|
||||
# again on call to next_page
|
||||
|
||||
# Throw a error if it is not possible (overflow)
|
||||
self.page += 1
|
||||
raw_page = self.callPirateBaT()
|
||||
return self.parse_raw_page_for_torrents(raw_page)
|
||||
|
||||
def set_total_pages(self, raw_page):
|
||||
# body-id:searchResults-id:content-align:center
|
||||
soup = BeautifulSoup(raw_page, 'html.parser')
|
||||
content_searchResult = soup.body.find(id='SearchResults')
|
||||
page_div = content_searchResult.find_next(attrs={"align": "center"})
|
||||
|
||||
last_page = 0
|
||||
for page in page_div.find_all('a'):
|
||||
last_page += 1
|
||||
|
||||
self.total_pages = last_page
|
||||
|
||||
def callPirateBaT(self):
|
||||
req = self.build_URL_request()
|
||||
|
||||
raw_page = self.fetchURL(req).read()
|
||||
logging.info('Finished searching piratebay for query | %s' % stringTime())
|
||||
|
||||
if raw_page is None:
|
||||
raise ValueError('Search result returned no content. Please check log for error reason.')
|
||||
|
||||
if self.total_pages is 0:
|
||||
self.set_total_pages(raw_page)
|
||||
|
||||
return raw_page
|
||||
|
||||
|
||||
# Sets the search
|
||||
def search(self, query, multiple_pages=1, page=0, sort=None, category=None):
|
||||
# This should not be logged here, but in loop. Something else here maybe?
|
||||
logging.info('Searching piratebay with query: %r, sort: %s and category: %s | %s' %
|
||||
(query, sort, category, stringTime()))
|
||||
|
||||
if sort is not None and sort in self.sortTypes:
|
||||
self.sort = self.sortTypes[sort]
|
||||
else:
|
||||
raise ValueError('Invalid sort category for piratebay search')
|
||||
|
||||
# Verify input? and reset total_pages
|
||||
self.query = query
|
||||
|
||||
self.total_pages = 0
|
||||
|
||||
if str(page).isnumeric() and type(page) == int and page >= 0:
|
||||
self.page = page
|
||||
|
||||
# TODO add category list
|
||||
if category is not None and category in self.categoryTypes:
|
||||
self.category = self.categoryTypes[category]
|
||||
|
||||
# TODO Pull most of this logic out bc it needs to also be done in next_page
|
||||
|
||||
raw_page = self.callPirateBaT()
|
||||
torrents_found = self.parse_raw_page_for_torrents(raw_page)
|
||||
|
||||
# Fetch in parallel
|
||||
n = pagesToCount(multiple_pages, self.total_pages)
|
||||
while n > 1:
|
||||
torrents_found.extend(self.next_page())
|
||||
n -= 1
|
||||
|
||||
return torrents_found
|
||||
|
||||
|
||||
def removeHeader(self, bs4_element):
|
||||
if ('header' in bs4_element['class']):
|
||||
return bs4_element.find_next('tr')
|
||||
|
||||
return bs4_element
|
||||
|
||||
def has_magnet(self, href):
|
||||
return href and re.compile('magnet').search(href)
|
||||
|
||||
def parse_raw_page_for_torrents(self, content):
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
content_searchResult = soup.body.find(id='searchResult')
|
||||
|
||||
if content_searchResult is None:
|
||||
logging.info('No torrents found for the search criteria.')
|
||||
return None
|
||||
|
||||
listElements = content_searchResult.tr
|
||||
|
||||
torrentWrapper = self.removeHeader(listElements)
|
||||
|
||||
torrents_found = []
|
||||
for torrentElement in torrentWrapper.find_all_next('td'):
|
||||
if torrentElement.find_all("div", class_='detName'):
|
||||
|
||||
name = torrentElement.find('a', class_='detLink').get_text()
|
||||
url = torrentElement.find('a', class_='detLink')['href']
|
||||
magnet = torrentElement.find(href=self.has_magnet)
|
||||
|
||||
uploader = torrentElement.find('a', class_='detDesc')
|
||||
|
||||
if uploader is None:
|
||||
uploader = torrentElement.find('i')
|
||||
|
||||
uploader = uploader.get_text()
|
||||
|
||||
info_text = torrentElement.find('font', class_='detDesc').get_text()
|
||||
|
||||
date = return_re_match(info_text, r"(\d+\-\d+(\s\d{4})?)|(Y\-day|Today)")
|
||||
size = return_re_match(info_text, r"(\d+(\.\d+)?\s[a-zA-Z]+)")
|
||||
|
||||
# COULD NOT FIND HREF!
|
||||
if (magnet is None):
|
||||
continue
|
||||
|
||||
seed_and_leech = torrentElement.find_all_next(attrs={"align": "right"})
|
||||
seed = seed_and_leech[0].get_text()
|
||||
leech = seed_and_leech[1].get_text()
|
||||
|
||||
torrent = Torrent(name, magnet['href'], size, uploader, date, seed, leech, url)
|
||||
|
||||
torrents_found.append(torrent)
|
||||
else:
|
||||
# print(torrentElement)
|
||||
continue
|
||||
|
||||
logging.info('Found %s torrents for given search criteria.' % len(torrents_found))
|
||||
return torrents_found
|
||||
|
||||
|
||||
def fetchURL(self, req):
|
||||
try:
|
||||
response = request.urlopen(req)
|
||||
except URLError as e:
|
||||
if hasattr(e, 'reason'):
|
||||
logging.error('We failed to reach a server with request: %s' % req.full_url)
|
||||
logging.error('Reason: %s' % e.reason)
|
||||
elif hasattr(e, 'code'):
|
||||
logging.error('The server couldn\'t fulfill the request.')
|
||||
logging.error('Error code: ', e.code)
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
class Torrent(object):
|
||||
def __init__(self, name, magnet=None, size=None, uploader=None, date=None,
|
||||
seed_count=None, leech_count=None, url=None):
|
||||
self.name = name
|
||||
self.magnet = magnet
|
||||
self.size = size
|
||||
self.uploader = uploader
|
||||
self.date = date
|
||||
self.seed_count = seed_count
|
||||
self.leech_count = leech_count
|
||||
self.url = url
|
||||
|
||||
def find_release_type(self):
|
||||
name = self.name.casefold()
|
||||
return [r_type for r_type in RELEASE_TYPES if r_type in name]
|
||||
|
||||
def get_all_attr(self):
|
||||
return ({'name': self.name, 'magnet': self.magnet,'uploader': self.uploader,'size': self.size,'date': self.date,'seed': self.seed_count,'leech': self.leech_count,'url': self.url})
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.name)
|
||||
|
||||
|
||||
# This should be done front_end!
|
||||
# I.E. filtering like this should be done in another script
|
||||
# and should be done with the shared standard for types.
|
||||
# PS: Is it the right move to use a shared standard? What
|
||||
# happens if it is no longer public?
|
||||
def chooseCandidate(torrent_list):
|
||||
interesting_torrents = []
|
||||
match_release_type = ['bdremux', 'brremux', 'remux', 'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5']
|
||||
|
||||
for torrent in torrent_list:
|
||||
intersecting_release_types = set(torrent.find_release_type()) & set(match_release_type)
|
||||
|
||||
size, _, size_id = torrent.size.partition(' ')
|
||||
if intersecting_release_types and int(torrent.seed_count) > 0 and float(size) > 4 and size_id == 'GiB':
|
||||
# print('{} : {} : {} {}'.format(torrent.name, torrent.size, torrent.seed_count, torrent.magnet))
|
||||
interesting_torrents.append(torrent.get_all_attr())
|
||||
# else:
|
||||
# print('Denied match! %s : %s : %s' % (torrent.name, torrent.size, torrent.seed_count))
|
||||
|
||||
return interesting_torrents
|
||||
|
||||
|
||||
def searchTorrentSite(query, site='piratebay'):
|
||||
pirate = piratebay()
|
||||
torrents_found = pirate.search(query, page=0, multiple_pages=3, sort='size')
|
||||
candidates = {}
|
||||
if (torrents_found):
|
||||
candidates = chooseCandidate(torrents_found)
|
||||
print(json.dumps(candidates))
|
||||
|
||||
# torrents_found = pirate.next_page()
|
||||
# pprint(torrents_found)
|
||||
# candidates = chooseCandidate(torrents_found)
|
||||
|
||||
# Can autocall to next_page in a looped way to get more if nothing is found
|
||||
# and there is more pages to be looked at
|
||||
|
||||
|
||||
def main():
|
||||
query = sys.argv[1]
|
||||
searchTorrentSite(query)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
57
.archive/app/seasonMover.py
Executable file
57
.archive/app/seasonMover.py
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-07-11 19:16:23
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-07-11 19:16:23
|
||||
|
||||
import fire, re, os
|
||||
|
||||
class seasonMover(object):
|
||||
''' Moving multiple files to multiple folders with
|
||||
identifer '''
|
||||
workingDir = os.getcwd()
|
||||
|
||||
def create(self, name, interval):
|
||||
pass
|
||||
|
||||
def move(self, fileSyntax, folderName):
|
||||
episodeRange = self.findInterval(fileSyntax)
|
||||
|
||||
self.motherMover(fileSyntax, folderName, episodeRange)
|
||||
|
||||
def findInterval(self, item):
|
||||
if (re.search(r'\((.*)\)', item) is None):
|
||||
raise ValueError('Need to declare an identifier e.g. (1..3) in: \n\t' + item)
|
||||
|
||||
start = int(re.search('\((\d+)\.\.', item).group(1))
|
||||
end = int(re.search('\.\.(\d+)\)', item).group(1))
|
||||
|
||||
return list(range(start, end+1))
|
||||
|
||||
def removeUploadSign(self, file):
|
||||
match = re.search('-[a-zA-Z\[\]\-]*.[a-z]{3}', file)
|
||||
if match:
|
||||
uploader = match.group(0)[:-4]
|
||||
return re.sub(uploader, '', file)
|
||||
|
||||
return file
|
||||
|
||||
def motherMover(self, fileSyntax, folderName, episodeRange):
|
||||
# Call for sub of fileList
|
||||
# TODO check if range is same as folderContent
|
||||
for episode in episodeRange:
|
||||
leadingZeroNumber = "%02d" % episode
|
||||
fileName = re.sub(r'\((.*)\)', leadingZeroNumber, fileSyntax)
|
||||
|
||||
oldPath = os.path.join(self.workingDir,fileName)
|
||||
newFolder = os.path.join(self.workingDir, folderName + leadingZeroNumber)
|
||||
newPath = os.path.join(newFolder, self.removeUploadSign(fileName))
|
||||
|
||||
os.makedirs(newFolder)
|
||||
os.rename(oldPath, newPath)
|
||||
# print(newFolder)
|
||||
# print(oldPath + ' --> ' + newPath)
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(seasonMover)
|
||||
111
.archive/app/subtitle.py
Normal file
111
.archive/app/subtitle.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import logging
|
||||
import os
|
||||
|
||||
import chardet
|
||||
import hashlib
|
||||
|
||||
from video import Episode, Movie
|
||||
from utils import sanitize
|
||||
|
||||
from langdetect import detect
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Subtitle extensions
|
||||
SUBTITLE_EXTENSIONS = ('.srt', '.sub')
|
||||
|
||||
|
||||
class Subtitle(object):
|
||||
"""Base class for subtitle.
|
||||
|
||||
:param language: language of the subtitle.
|
||||
:type language: :class:`~babelfish.language.Language`
|
||||
:param bool hearing_impaired: whether or not the subtitle is hearing impaired.
|
||||
:param page_link: URL of the web page from which the subtitle can be downloaded.
|
||||
:type page_link: str
|
||||
:param encoding: Text encoding of the subtitle.
|
||||
:type encoding: str
|
||||
|
||||
"""
|
||||
#: Name of the provider that returns that class of subtitle
|
||||
provider_name = ''
|
||||
|
||||
def __init__(self, name, parent_path, series, season, episode, language=None, hash=None, container=None, format=None, sdh=False):
|
||||
#: Language of the subtitle
|
||||
|
||||
self.name = name
|
||||
|
||||
self.parent_path = parent_path
|
||||
|
||||
self.series = series
|
||||
|
||||
self.season = season
|
||||
|
||||
self.episode = episode
|
||||
|
||||
self.language=language
|
||||
|
||||
self.hash = hash
|
||||
|
||||
self.container = container
|
||||
|
||||
self.format = format
|
||||
|
||||
self.sdh = sdh
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, parent_path, guess):
|
||||
if not (guess['type'] == 'movie' or guess['type'] == 'episode'):
|
||||
raise ValueError('The guess must be an episode guess')
|
||||
|
||||
if 'title' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
|
||||
sdh = 'sdh' in name.lower()
|
||||
|
||||
if guess['type'] is 'episode':
|
||||
return cls(name, parent_path, guess.get('title', 1), guess.get('season'), guess['episode'],
|
||||
container=guess.get('container'), format=guess.get('format'), sdh=sdh)
|
||||
elif guess['type'] is 'movie':
|
||||
return cls(name, parent_path, guess.get('title', 1), container=guess.get('container'),
|
||||
format=guess.get('format'), sdh=sdh)
|
||||
|
||||
|
||||
def getLanguage(self):
|
||||
f = open(os.path.join(self.parent_path, self.name), 'r', encoding='ISO-8859-15')
|
||||
language = detect(f.read())
|
||||
f.close()
|
||||
|
||||
return language
|
||||
|
||||
def __hash__(self):
|
||||
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s [%sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
|
||||
|
||||
|
||||
|
||||
def get_subtitle_path(subtitles_path, language=None, extension='.srt'):
|
||||
"""Get the subtitle path using the `subtitles_path` and `language`.
|
||||
|
||||
:param str subtitles_path: path to the subtitle.
|
||||
:param language: language of the subtitle to put in the path.
|
||||
:type language: :class:`~babelfish.language.Language`
|
||||
:param str extension: extension of the subtitle.
|
||||
:return: path of the subtitle.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
subtitle_root = os.path.splitext(subtitles_path)[0]
|
||||
|
||||
if language:
|
||||
subtitle_root += '.' + str(language)
|
||||
|
||||
return subtitle_root + extension
|
||||
|
||||
|
||||
|
||||
38
.archive/app/utils.py
Normal file
38
.archive/app/utils.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
|
||||
def sanitize(string, ignore_characters=None):
|
||||
"""Sanitize a string to strip special characters.
|
||||
|
||||
:param str string: the string to sanitize.
|
||||
:param set ignore_characters: characters to ignore.
|
||||
:return: the sanitized string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# only deal with strings
|
||||
if string is None:
|
||||
return
|
||||
|
||||
ignore_characters = ignore_characters or set()
|
||||
|
||||
# replace some characters with one space
|
||||
# characters = {'-', ':', '(', ')', '.'} - ignore_characters
|
||||
# if characters:
|
||||
# string = re.sub(r'[%s]' % re.escape(''.join(characters)), ' ', string)
|
||||
|
||||
# remove some characters
|
||||
characters = {'\''} - ignore_characters
|
||||
if characters:
|
||||
string = re.sub(r'[%s]' % re.escape(''.join(characters)), '', string)
|
||||
|
||||
# replace multiple spaces with one
|
||||
string = re.sub(r'\s+', ' ', string)
|
||||
|
||||
# strip and lower case
|
||||
return string.strip().lower()
|
||||
|
||||
233
.archive/app/video.py
Normal file
233
.archive/app/video.py
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-08-26 08:23:18
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-09-29 13:56:21
|
||||
|
||||
from guessit import guessit
|
||||
import os
|
||||
import hashlib, tvdb_api
|
||||
|
||||
#: Video extensions
|
||||
VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik',
|
||||
'.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli',
|
||||
'.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e',
|
||||
'.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4',
|
||||
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm' '.ogv', '.omf',
|
||||
'.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo',
|
||||
'.vob', '.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
|
||||
|
||||
class Video(object):
|
||||
"""Base class for videos.
|
||||
Represent a video, existing or not.
|
||||
:param str name: name or path of the video.
|
||||
:param str format: format of the video (HDTV, WEB-DL, BluRay, ...).
|
||||
:param str release_group: release group of the video.
|
||||
:param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i).
|
||||
:param str video_codec: codec of the video stream.
|
||||
:param str audio_codec: codec of the main audio stream.
|
||||
:param str imdb_id: IMDb id of the video.
|
||||
:param dict hashes: hashes of the video file by provider names.
|
||||
:param int size: size of the video file in bytes.
|
||||
:param set subtitle_languages: existing subtitle languages.
|
||||
"""
|
||||
def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None,
|
||||
imdb_id=None, hashes=None, size=None, subtitle_languages=None):
|
||||
#: Name or path of the video
|
||||
self.name = name
|
||||
|
||||
#: Format of the video (HDTV, WEB-DL, BluRay, ...)
|
||||
self.format = format
|
||||
|
||||
#: Release group of the video
|
||||
self.release_group = release_group
|
||||
|
||||
#: Resolution of the video stream (480p, 720p, 1080p or 1080i)
|
||||
self.resolution = resolution
|
||||
|
||||
#: Codec of the video stream
|
||||
self.video_codec = video_codec
|
||||
|
||||
#: Codec of the main audio stream
|
||||
self.audio_codec = audio_codec
|
||||
|
||||
#: IMDb id of the video
|
||||
self.imdb_id = imdb_id
|
||||
|
||||
#: Hashes of the video file by provider names
|
||||
self.hashes = hashes or {}
|
||||
|
||||
#: Size of the video file in bytes
|
||||
self.size = size
|
||||
|
||||
#: Existing subtitle languages
|
||||
self.subtitle_languages = subtitle_languages or set()
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
"""Test whether the video exists"""
|
||||
return os.path.exists(self.name)
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
"""Age of the video"""
|
||||
if self.exists:
|
||||
return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name))
|
||||
|
||||
return timedelta()
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, parent_path, guess):
|
||||
"""Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`.
|
||||
:param str name: name of the video.
|
||||
:param dict guess: guessed data.
|
||||
:raise: :class:`ValueError` if the `type` of the `guess` is invalid
|
||||
"""
|
||||
if guess['type'] == 'episode':
|
||||
return Episode.fromguess(name, parent_path, guess)
|
||||
|
||||
if guess['type'] == 'movie':
|
||||
return Movie.fromguess(name, guess)
|
||||
|
||||
raise ValueError('The guess must be an episode or a movie guess')
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
"""Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`.
|
||||
:param str name: name of the video.
|
||||
"""
|
||||
return cls.fromguess(name, guessit(name))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.name)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Episode():
|
||||
"""Episode :class:`Video`.
|
||||
:param str series: series of the episode.
|
||||
:param int season: season number of the episode.
|
||||
:param int episode: episode number of the episode.
|
||||
:param str title: title of the episode.
|
||||
:param int year: year of the series.
|
||||
:param bool original_series: whether the series is the first with this name.
|
||||
:param int tvdb_id: TVDB id of the episode.
|
||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||
"""
|
||||
def __init__(self, name, parent_path, series, season, episode, year=None, original_series=True, tvdb_id=None,
|
||||
series_tvdb_id=None, series_imdb_id=None, release_group=None, video_codec=None, container=None,
|
||||
format=None, screen_size=None, **kwargs):
|
||||
super(Episode, self).__init__()
|
||||
|
||||
self.name = name
|
||||
|
||||
self.parent_path = parent_path
|
||||
|
||||
#: Series of the episode
|
||||
self.series = series
|
||||
|
||||
#: Season number of the episode
|
||||
self.season = season
|
||||
|
||||
#: Episode number of the episode
|
||||
self.episode = episode
|
||||
|
||||
#: Year of series
|
||||
self.year = year
|
||||
|
||||
#: The series is the first with this name
|
||||
self.original_series = original_series
|
||||
|
||||
#: TVDB id of the episode
|
||||
self.tvdb_id = tvdb_id
|
||||
|
||||
#: TVDB id of the series
|
||||
self.series_tvdb_id = series_tvdb_id
|
||||
|
||||
#: IMDb id of the series
|
||||
self.series_imdb_id = series_imdb_id
|
||||
|
||||
# The release group of the episode
|
||||
self.release_group = release_group
|
||||
|
||||
# The video vodec of the series
|
||||
self.video_codec = video_codec
|
||||
|
||||
# The Video container of the episode
|
||||
self.container = container
|
||||
|
||||
# The Video format of the episode
|
||||
self.format = format
|
||||
|
||||
# The Video screen_size of the episode
|
||||
self.screen_size = screen_size
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, parent_path, guess):
|
||||
if guess['type'] != 'episode':
|
||||
raise ValueError('The guess must be an episode guess')
|
||||
|
||||
if 'title' not in guess or 'episode' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
|
||||
return cls(name, parent_path, guess['title'], guess.get('season', 1), guess['episode'],
|
||||
year=guess.get('year'), original_series='year' not in guess, release_group=guess.get('release_group'),
|
||||
video_codec=guess.get('video_codec'), audio_codec=guess.get('audio_codec'), container=guess.get('container'),
|
||||
format=guess.get('format'), screen_size=guess.get('screen_size'))
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
return cls.fromguess(name, guessit(name, {'type': 'episode'}))
|
||||
|
||||
def __hash__(self):
|
||||
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
|
||||
|
||||
# THE EP NUMBER IS CONVERTED TO STRING AS A QUICK FIX FOR MULTIPLE NUMBERS IN ONE
|
||||
def __repr__(self):
|
||||
if self.year is None:
|
||||
return '<%s [%r, %sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
|
||||
|
||||
return '<%s [%r, %d, %sx%s]>' % (self.__class__.__name__, self.series, self.year, self.season, str(self.episode))
|
||||
|
||||
|
||||
|
||||
class Movie():
|
||||
"""Movie :class:`Video`.
|
||||
:param str title: title of the movie.
|
||||
:param int year: year of the movie.
|
||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||
"""
|
||||
def __init__(self, name, title, year=None, format=None, **kwargs):
|
||||
super(Movie, self).__init__()
|
||||
|
||||
#: Title of the movie
|
||||
self.title = title
|
||||
|
||||
#: Year of the movie
|
||||
self.year = year
|
||||
self.format = format
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, guess):
|
||||
if guess['type'] != 'movie':
|
||||
raise ValueError('The guess must be a movie guess')
|
||||
|
||||
if 'title' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
|
||||
return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('release_group'),
|
||||
resolution=guess.get('screen_size'), video_codec=guess.get('video_codec'),
|
||||
audio_codec=guess.get('audio_codec'), year=guess.get('year'))
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
return cls.fromguess(name, guessit(name, {'type': 'movie'}))
|
||||
|
||||
def __repr__(self):
|
||||
if self.year is None:
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.title)
|
||||
|
||||
return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year)
|
||||
8
.archive/client/.babelrc
Normal file
8
.archive/client/.babelrc
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
./.babelrc
|
||||
*/
|
||||
{
|
||||
"presets":[
|
||||
"es2015", "env", "react"
|
||||
]
|
||||
}
|
||||
58
.archive/client/.gitignore
vendored
Normal file
58
.archive/client/.gitignore
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
BIN
.archive/client/app/DIN-Regular-webfont.woff
Normal file
BIN
.archive/client/app/DIN-Regular-webfont.woff
Normal file
Binary file not shown.
25
.archive/client/app/Root.jsx
Normal file
25
.archive/client/app/Root.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { Component } from 'react';
|
||||
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
|
||||
|
||||
import SearchRequest from './components/SearchRequest.jsx';
|
||||
import AdminComponent from './components/admin/Admin.jsx';
|
||||
|
||||
class Root extends Component {
|
||||
|
||||
// We need to provide a list of routes
|
||||
// for our app, and in this case we are
|
||||
// doing so from a Root component
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SearchRequest} />
|
||||
<Route path='/admin/:request' component={AdminComponent} />
|
||||
<Route path='/admin' component={AdminComponent} />
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Root;
|
||||
41
.archive/client/app/app.scss
Normal file
41
.archive/client/app/app.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@font-face {
|
||||
font-family: "din";
|
||||
src: url('/app/DIN-Regular-webfont.woff')
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'din', 'Open Sans', sans-serif;
|
||||
display: inline-block;
|
||||
color:red;
|
||||
}
|
||||
|
||||
#requestMovieList {
|
||||
display: flex;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.movie_wrapper {
|
||||
color:red;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
|
||||
width: 30%;
|
||||
background-color: #ffffff;
|
||||
height: 231px;
|
||||
|
||||
margin: 20px;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
|
||||
-moz-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
|
||||
box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.movie_content {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.movie_header {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
26
.archive/client/app/components/Cookie.jsx
Normal file
26
.archive/client/app/components/Cookie.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for(var i = 0; i <ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
||||
var expires = "expires="+ d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
63
.archive/client/app/components/FetchData.js
Normal file
63
.archive/client/app/components/FetchData.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
class FetchData extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
playing: [],
|
||||
hei: '1',
|
||||
intervalId: null,
|
||||
url: ''
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
var that = this;
|
||||
fetch("https://apollo.kevinmidboe.com/api/v1/plex/playing").then(
|
||||
function(response){
|
||||
response.json().then(function(data){
|
||||
that.setState({
|
||||
playing: that.state.playing.concat(data.video)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// use intervalId from the state to clear the interval
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
|
||||
getPlaying() {
|
||||
if (this.state.playing.length != 0) {
|
||||
return this.state.playing.map((playingObj) => {
|
||||
if (playingObj.type === 'episode') {
|
||||
console.log('episode')
|
||||
return ([
|
||||
<span>{playingObj.title}</span>,
|
||||
<span>{playingObj.season}</span>,
|
||||
<span>{playingObj.episode}</span>
|
||||
])
|
||||
} else if (playingObj.type === 'movie') {
|
||||
console.log('movie')
|
||||
return ([
|
||||
<span>{playingObj.title}</span>
|
||||
])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return (<span>Nothing playing</span>)
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div className="FetchData">{this.getPlaying()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default FetchData;
|
||||
266
.archive/client/app/components/FetchRequested.jsx
Normal file
266
.archive/client/app/components/FetchRequested.jsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import React from 'react';
|
||||
|
||||
import requestElement from './styles/requestElementStyle.jsx'
|
||||
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
class DropdownList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filter: ['all', 'requested', 'downloading', 'downloaded'],
|
||||
sort: ['requested_date', 'name', 'status', 'requested_by', 'ip', 'user_agent'],
|
||||
status: ['requested', 'downloading', 'downloaded'],
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {elementType, elementId, elementStatus, elementCallback, props} = this.props;
|
||||
|
||||
console.log(elementCallback('downloaded'))
|
||||
|
||||
switch (elementType) {
|
||||
case 'status':
|
||||
return (
|
||||
<div>HERE</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RequestElement extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dropDownState: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
filterRequestList(requestList, filter) {
|
||||
if (filter === 'all')
|
||||
return requestList
|
||||
|
||||
if (filter === 'movie' || filter === 'show')
|
||||
return requestList.filter(item => item.type === filter)
|
||||
return requestList.filter(item => item.status === filter)
|
||||
}
|
||||
|
||||
sortRequestList(requestList, sort_type, reversed) {
|
||||
requestList.sort(function(a,b) {
|
||||
if(a[sort_type] < b[sort_type]) return -1;
|
||||
if(a[sort_type] > b[sort_type]) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (reversed)
|
||||
requestList.reverse();
|
||||
}
|
||||
|
||||
userAgent(agent) {
|
||||
if (agent) {
|
||||
try {
|
||||
return agent.split(" ")[1].replace(/[\(\;]/g, '');
|
||||
}
|
||||
catch(e) {
|
||||
return agent;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
updateDropDownState(status) {
|
||||
if (status !== this.dropDownState) {
|
||||
this.dropDownState = status;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ItemsStatusDropdown(id, type, status) {
|
||||
return (
|
||||
<div>
|
||||
<select id="lang"
|
||||
defaultValue={status}
|
||||
onChange={event => this.updateDropDownState(event.target.value)}
|
||||
>
|
||||
<option value='requested'>Requested</option>
|
||||
<option value='downloading'>Downloading</option>
|
||||
<option value='downloaded'>Downloaded</option>
|
||||
</select>
|
||||
|
||||
<button onClick={() => { this.updateRequestedItem(id, type)}}>Update Status</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
updateRequestedItem(id, type) {
|
||||
console.log(id, type, this.dropDownState);
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
'authorization': getCookie('token')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: type,
|
||||
status: this.dropDownState,
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status !== 200) {
|
||||
console.log('error');
|
||||
}
|
||||
|
||||
response.json()
|
||||
.then(data => {
|
||||
if (data.success === true) {
|
||||
console.log('UPDATED :', id, ' with ', this.dropDownState)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
new Error(error);
|
||||
})
|
||||
}
|
||||
|
||||
createHTMLElement(data, index) {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w300' + data.image_path;
|
||||
|
||||
return (
|
||||
<div style={requestElement.wrappingDiv} key={index}>
|
||||
<img style={requestElement.requestPoster} src={posterPath}></img>
|
||||
<div style={requestElement.infoDiv}>
|
||||
<span><b>Name</b>: {data.name} </span><br></br>
|
||||
<span><b>Year</b>: {data.year}</span><br></br>
|
||||
<span><b>Type</b>: {data.type}</span><br></br>
|
||||
<span><b>Status</b>: {data.status}</span><br></br>
|
||||
<span><b>Address</b>: {data.ip}</span><br></br>
|
||||
<span><b>Requested Data:</b> {data.requested_date}</span><br></br>
|
||||
<span><b>Requested By:</b> {data.requested_by}</span><br></br>
|
||||
<span><b>Agent</b>: { this.userAgent(data.user_agent) }</span><br></br>
|
||||
</div>
|
||||
|
||||
{ this.ItemsStatusDropdown(data.id, data.type, data.status) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props;
|
||||
|
||||
var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter)
|
||||
|
||||
this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed)
|
||||
|
||||
return (
|
||||
<div {...props} style={requestElement.bodyDiv}>
|
||||
{filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FetchRequested extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
requested_objects: [],
|
||||
filter: 'all',
|
||||
sort: {
|
||||
value: 'requested_date',
|
||||
reversed: false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
'authorization': getCookie('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status !== 200) {
|
||||
console.log('error');
|
||||
}
|
||||
|
||||
response.json()
|
||||
.then(data => {
|
||||
if (data.success === true) {
|
||||
this.setState({
|
||||
requested_objects: data.requestedItems
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
new Error(error);
|
||||
})
|
||||
}
|
||||
|
||||
changeFilter(filter) {
|
||||
this.setState({
|
||||
filter: filter
|
||||
})
|
||||
}
|
||||
|
||||
updateSort(sort=null, reverse=false) {
|
||||
if (sort) {
|
||||
this.setState({
|
||||
sort: { value: sort, reversed: reverse }
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
sort: { value: this.state.sort.value, reversed: reverse }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<select id="lang" onChange={event => this.changeFilter(event.target.value)} value={this.state.value}>
|
||||
<option value="all">All</option>
|
||||
<option value="requested">Requested</option>
|
||||
<option value="downloading">Downloading</option>
|
||||
<option value="downloaded">Downloaded</option>
|
||||
<option value='movie'>Movies</option>
|
||||
<option value='show'>Shows</option>
|
||||
</select>
|
||||
|
||||
<select id="lang" onChange={event => this.updateSort(event.target.value)} value={this.state.value}>
|
||||
<option value='requested_date'>Date</option>
|
||||
<option value='name'>Title</option>
|
||||
<option value='status'>Status</option>
|
||||
<option value='requested_by'>Requested By</option>
|
||||
<option value='ip'>Address</option>
|
||||
<option value='user_agent'>Agent</option>
|
||||
</select>
|
||||
|
||||
<button onClick={() => {this.updateSort(null, !this.state.sort.reversed)}}>Reverse</button>
|
||||
|
||||
<RequestElement
|
||||
requestedElementsList={this.state.requested_objects}
|
||||
requestedElementsFilter={this.state.filter}
|
||||
requestedElementsSort={this.state.sort}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FetchRequested;
|
||||
11
.archive/client/app/components/Header.jsx
Normal file
11
.archive/client/app/components/Header.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
// The Header creates links that can be used to navigate
|
||||
// between routes.
|
||||
const Header = () => (
|
||||
<header>
|
||||
</header>
|
||||
)
|
||||
|
||||
export default Header
|
||||
44
.archive/client/app/components/ListStrays.jsx
Normal file
44
.archive/client/app/components/ListStrays.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
class ListStrays extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
strays: [],
|
||||
hei: '1'
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
var that = this;
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/seasoned/all').then(
|
||||
function(response){
|
||||
response.json().then(function(data){
|
||||
// console.log(data);
|
||||
that.setState({
|
||||
strays: that.state.strays.concat(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div className="ListStrays">
|
||||
{this.state.strays.map((strayObj) => {
|
||||
if (strayObj.verified == 0) {
|
||||
var url = "https://kevinmidboe.com/seasoned/verified.html?id=" + strayObj.id;
|
||||
return ([
|
||||
<span key={strayObj.id}>{strayObj.name}</span>,
|
||||
<a href={url}>{strayObj.id}</a>
|
||||
])
|
||||
}
|
||||
})}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ListStrays;
|
||||
10
.archive/client/app/components/NotFound.js
Normal file
10
.archive/client/app/components/NotFound.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// components/NotFound.js
|
||||
import React from 'react';
|
||||
|
||||
const NotFound = () =>
|
||||
<div>
|
||||
<h3>404 page not found</h3>
|
||||
<p>We are sorry but the page you are looking for does not exist.</p>
|
||||
</div>
|
||||
|
||||
export default NotFound;
|
||||
126
.archive/client/app/components/SearchObject.jsx
Normal file
126
.archive/client/app/components/SearchObject.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
|
||||
import Notifications, {notify} from 'react-notify-toast';
|
||||
|
||||
// StyleComponents
|
||||
import searchObjectCSS from './styles/searchObject.jsx';
|
||||
import buttonsCSS from './styles/buttons.jsx';
|
||||
import InfoButton from './buttons/InfoButton.jsx';
|
||||
|
||||
var MediaQuery = require('react-responsive');
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
|
||||
class SearchObject {
|
||||
constructor(object) {
|
||||
this.id = object.id;
|
||||
this.title = object.title;
|
||||
this.year = object.year;
|
||||
this.type = object.type;
|
||||
this.rating = object.rating;
|
||||
this.poster = object.poster_path;
|
||||
this.background = object.background_path;
|
||||
this.matchedInPlex = object.matchedInPlex;
|
||||
this.summary = object.summary;
|
||||
}
|
||||
|
||||
requestExisting(movie) {
|
||||
console.log('Exists', movie);
|
||||
}
|
||||
|
||||
requestMovie() {
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
notify.show(this.title + ' requested!', 'success', 3000);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Request movie fetch went wrong: '+ e);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
getElement(index) {
|
||||
const element_key = index + this.id;
|
||||
|
||||
if (this.poster == null || this.poster == undefined) {
|
||||
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
||||
} else {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w185' + this.poster;
|
||||
}
|
||||
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
|
||||
|
||||
var foundInPlex;
|
||||
if (this.matchedInPlex) {
|
||||
foundInPlex = <Interactive
|
||||
as='button'
|
||||
onClick={() => {this.requestExisting(this)}}
|
||||
style={buttonsCSS.submit}
|
||||
focus={buttonsCSS.submit_hover}
|
||||
hover={buttonsCSS.submit_hover}>
|
||||
|
||||
<span>Request Anyway</span>
|
||||
</Interactive>;
|
||||
} else {
|
||||
foundInPlex = <Interactive
|
||||
as='button'
|
||||
onClick={() => {this.requestMovie()}}
|
||||
style={buttonsCSS.submit}
|
||||
focus={buttonsCSS.submit_hover}
|
||||
hover={buttonsCSS.submit_hover}>
|
||||
|
||||
<span>+ Request</span>
|
||||
</Interactive>;
|
||||
}
|
||||
|
||||
// TODO go away from using mediaQuery, and create custom resizer
|
||||
return (
|
||||
<div key={element_key}>
|
||||
<Notifications />
|
||||
|
||||
<div style={searchObjectCSS.container} key={this.id}>
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={searchObjectCSS.posterContainer}>
|
||||
<img style={searchObjectCSS.posterImage} id='poster' src={posterPath}></img>
|
||||
</div>
|
||||
<span style={searchObjectCSS.title_large}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.stats_large}>
|
||||
Released: { this.year } | Rating: {this.rating} | Type: {this.type}
|
||||
</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.summary}>{this.summary}</span>
|
||||
<br></br>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<img src={ backgroundPath } style={searchObjectCSS.backgroundImage}></img>
|
||||
<span style={searchObjectCSS.title_small}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.stats_small}>Released: {this.year} | Rating: {this.rating}</span>
|
||||
</MediaQuery>
|
||||
|
||||
<div style={searchObjectCSS.buttons}>
|
||||
{foundInPlex}
|
||||
|
||||
<InfoButton id={this.id} type={this.type} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<br />
|
||||
</MediaQuery>
|
||||
|
||||
<div style={searchObjectCSS.dividerRow}>
|
||||
<div style={searchObjectCSS.itemDivider}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchObject;
|
||||
464
.archive/client/app/components/SearchRequest.jsx
Normal file
464
.archive/client/app/components/SearchRequest.jsx
Normal file
@@ -0,0 +1,464 @@
|
||||
import React from 'react';
|
||||
|
||||
import URI from 'urijs';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
|
||||
// StyleComponents
|
||||
import searchRequestCSS from './styles/searchRequestStyle.jsx';
|
||||
|
||||
import SearchObject from './SearchObject.jsx';
|
||||
import Loading from './images/loading.jsx'
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
var MediaQuery = require('react-responsive');
|
||||
|
||||
// TODO add option for searching multi, movies or tv shows
|
||||
class SearchRequest extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
// Constructor with states holding the search query and the element of reponse.
|
||||
this.state = {
|
||||
lastApiCallURI: '',
|
||||
searchQuery: '',
|
||||
responseMovieList: null,
|
||||
movieFilter: false,
|
||||
showFilter: false,
|
||||
discoverType: '',
|
||||
page: 1,
|
||||
resultHeader: '',
|
||||
loadResults: false,
|
||||
scrollHasMore: true,
|
||||
loading: false,
|
||||
}
|
||||
|
||||
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
|
||||
|
||||
this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb/list';
|
||||
// this.baseUrl = 'http://localhost:31459/api/v1/tmdb/list';
|
||||
this.searchUrl = 'https://apollo.kevinmidboe.com/api/v1/plex/request';
|
||||
// this.searchUrl = 'http://localhost:31459/api/v1/plex/request';
|
||||
}
|
||||
|
||||
|
||||
componentWillMount(){
|
||||
var that = this;
|
||||
// this.setState({responseMovieList: null})
|
||||
this.resetPageNumber();
|
||||
this.state.loadResults = true;
|
||||
this.fetchTmdbList(this.allowedListTypes[Math.floor(Math.random()*this.allowedListTypes.length)]);
|
||||
}
|
||||
|
||||
// Handles all errors of the response of a fetch call
|
||||
handleErrors(response) {
|
||||
if (!response.ok)
|
||||
throw Error(response.status);
|
||||
return response;
|
||||
}
|
||||
|
||||
handleQueryError(response) {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
this.setState({
|
||||
responseMovieList: <h1>Nothing found for search query: { this.findQueryInURI(uri) }</h1>
|
||||
})
|
||||
}
|
||||
console.log('handleQueryError: ', error);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Unpacks the query value of a uri
|
||||
findQueryValueInURI(uri) {
|
||||
let uriSearchValues = uri.query(true);
|
||||
let queryValue = uriSearchValues['query']
|
||||
|
||||
return queryValue;
|
||||
}
|
||||
|
||||
// Unpacks the page value of a uri
|
||||
findPageValueInURI(uri) {
|
||||
let uriSearchValues = uri.query(true);
|
||||
let queryValue = uriSearchValues['page']
|
||||
|
||||
return queryValue;
|
||||
}
|
||||
|
||||
resetPageNumber() {
|
||||
this.state.page = 1;
|
||||
}
|
||||
|
||||
setLoading(value) {
|
||||
this.setState({
|
||||
loading: value
|
||||
});
|
||||
}
|
||||
|
||||
// Test this by calling missing endpoint or 404 query and see what code
|
||||
// and filter the error message based on the code.
|
||||
// Calls a uri and returns the response as json
|
||||
callURI(uri, method, data={}) {
|
||||
return fetch(uri, {
|
||||
method: method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'authorization': getCookie('token'),
|
||||
'loggedinuser': getCookie('loggedInUser'),
|
||||
})
|
||||
})
|
||||
.then(response => { return response })
|
||||
.catch((error) => {
|
||||
throw Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Saves the input string as a h1 element in responseMovieList state
|
||||
fillResponseMovieListWithError(msg) {
|
||||
this.setState({
|
||||
responseMovieList: <h1>{ msg }</h1>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Here we first call api for a search with the input uri, handle any errors
|
||||
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
|
||||
callSearchFillMovieList(uri) {
|
||||
Promise.resolve()
|
||||
.then(() => this.callURI(uri, 'GET'))
|
||||
.then(response => {
|
||||
// If we get a error code for the request
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
if (this.findPageValueInURI(new URI(response.url)) > 1) {
|
||||
this.state.scrollHasMore = false;
|
||||
console.log(this.state.scrollHasMore)
|
||||
return null
|
||||
let returnMessage = 'this is the return mesasge than will never be delivered'
|
||||
let theSecondReturnMsg = 'this is the second return messag ethat will NEVE be delivered'
|
||||
}
|
||||
else {
|
||||
|
||||
let errorMsg = 'Nothing found for the search query: ' + this.findQueryValueInURI(uri);
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
}
|
||||
else {
|
||||
let errorMsg = 'Error fetching query from server ' + this.response.status;
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||
// mapped as a SearchObject.
|
||||
response.json()
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
this.setState({
|
||||
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
} else {
|
||||
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
|
||||
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
|
||||
this.setState({
|
||||
responseMovieList: growingReponseMovieObjectList,
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('CallSearchFillMovieList: ', error)
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Something went wrong when fetching query.', error)
|
||||
})
|
||||
}
|
||||
|
||||
callListFillMovieList(uri) {
|
||||
// Write loading animation
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => this.callURI(uri, 'GET', undefined))
|
||||
.then(response => {
|
||||
// If we get a error code for the request
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
let errorMsg = 'List not found';
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
else {
|
||||
let errorMsg = 'Error fetching list from server ' + this.response.status;
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||
// mapped as a SearchObject.
|
||||
response.json()
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
this.setState({
|
||||
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
} else {
|
||||
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
|
||||
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
|
||||
this.setState({
|
||||
responseMovieList: growingReponseMovieObjectList,
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Something went wrong when fetching query.', error)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
searchSeasonedRequest() {
|
||||
this.state.resultHeader = 'Search result for: ' + this.state.searchQuery;
|
||||
|
||||
// Build uri with the url for searching requests
|
||||
var uri = new URI(this.searchUrl);
|
||||
// Add input of search query and page count to the uri payload
|
||||
uri = uri.search({ 'query': this.state.searchQuery, 'page': this.state.page });
|
||||
|
||||
if (this.state.showFilter)
|
||||
uri = uri.addSearch('type', 'show');
|
||||
else
|
||||
if (this.state.movieFilter)
|
||||
uri = uri.addSearch('type', 'movie')
|
||||
|
||||
// Send uri to call and fill the response list with movie/show objects
|
||||
this.callSearchFillMovieList(uri);
|
||||
}
|
||||
|
||||
fetchTmdbList(tmdbListType) {
|
||||
console.log(tmdbListType)
|
||||
// Check if it is a whitelisted list, this should be replaced with checking if the return call is 500
|
||||
if (this.allowedListTypes.indexOf(tmdbListType) === -1)
|
||||
throw Error('Invalid discover type: ' + tmdbListType);
|
||||
|
||||
this.state.responseMovieList = []
|
||||
// Captialize the first letter of and save the discoverQueryType to resultHeader state.
|
||||
this.state.resultHeader = tmdbListType.toLowerCase().replace(/\b[a-z]/g, function(letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
|
||||
// Build uri with the url for searching requests
|
||||
var uri = new URI(this.baseUrl);
|
||||
uri.segment(tmdbListType);
|
||||
// Add input of search query and page count to the uri payload
|
||||
uri = uri.search({ 'page': this.state.page });
|
||||
|
||||
if (this.state.showFilter)
|
||||
uri = uri.addSearch('type', 'show');
|
||||
|
||||
// Send uri to call and fill the response list with movie/show objects
|
||||
this.callListFillMovieList(uri);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Updates the internal state of the query search field.
|
||||
updateQueryState(event){
|
||||
this.setState({
|
||||
searchQuery: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
// For checking if the enter key was pressed in the search field.
|
||||
_handleQueryKeyPress(e) {
|
||||
if (e.key === 'Enter') {
|
||||
// this.fetchQuery();
|
||||
// Reset page number for a new search
|
||||
this.resetPageNumber();
|
||||
this.searchSeasonedRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// When called passes the variable to SearchObject and calls it's interal function for
|
||||
// generating the wanted HTML
|
||||
createMovieObjects(item, index) {
|
||||
let movie = new SearchObject(item);
|
||||
return movie.getElement(index);
|
||||
}
|
||||
|
||||
toggleFilter(filterType) {
|
||||
if (filterType == 'movies') {
|
||||
this.setState({
|
||||
movieFilter: !this.state.movieFilter
|
||||
})
|
||||
console.log(this.state.movieFilter);
|
||||
}
|
||||
else if (filterType == 'shows') {
|
||||
this.setState({
|
||||
showFilter: !this.state.showFilter
|
||||
})
|
||||
console.log(this.state.showFilter);
|
||||
}
|
||||
}
|
||||
|
||||
pageBackwards() {
|
||||
if (this.state.page > 1) {
|
||||
let pageNumber = this.state.page - 1;
|
||||
let uri = this.state.lastApiCallURI;
|
||||
|
||||
// Augment the page number of the uri with a callback
|
||||
uri.search(function(data) {
|
||||
data.page = pageNumber;
|
||||
});
|
||||
|
||||
// Call the api with the new uri
|
||||
this.callSearchFillMovieList(uri);
|
||||
// Update state of our page number after the call is done
|
||||
this.state.page = pageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO need to get total page number and save in a state to not overflow
|
||||
pageForwards() {
|
||||
// Wrap this in the check
|
||||
let pageNumber = this.state.page + 1;
|
||||
let uri = this.state.lastApiCallURI;
|
||||
|
||||
// Augment the page number of the uri with a callback
|
||||
uri.search(function(data) {
|
||||
data.page = pageNumber;
|
||||
});
|
||||
|
||||
// Call the api with the new uri
|
||||
this.callSearchFillMovieList(uri);
|
||||
// Update state of our page number after the call is done
|
||||
this.state.page = pageNumber;
|
||||
}
|
||||
|
||||
movieToggle() {
|
||||
if (this.state.movieFilter)
|
||||
return <span style={searchRequestCSS.searchFilterActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('movies')}}
|
||||
id="category_active">Movies</span>
|
||||
else
|
||||
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('movies')}}
|
||||
id="category_active">Movies</span>
|
||||
}
|
||||
|
||||
showToggle() {
|
||||
if (this.state.showFilter)
|
||||
return <span style={searchRequestCSS.searchFilterActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('shows')}}
|
||||
id="category_active">TV Shows</span>
|
||||
else
|
||||
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('shows')}}
|
||||
id="category_active">TV Shows</span>
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
const loader = <div className="loader">Loading ...<br></br></div>;
|
||||
|
||||
|
||||
return(
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={this.pageForwards.bind(this)}
|
||||
hasMore={this.state.scrollHasMore}
|
||||
loader={<Loading />}
|
||||
initialLoad={this.state.loadResults}>
|
||||
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={searchRequestCSS.body}>
|
||||
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
|
||||
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content for plex</span>
|
||||
</div>
|
||||
|
||||
<div style={searchRequestCSS.searchLargeContainer}>
|
||||
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||
|
||||
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||
<div style={{marginLeft: '30px'}}>
|
||||
<div style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</div>
|
||||
<span style={{content: '', display: 'block', width: '2em', borderTop: '2px solid #000,'}}></span>
|
||||
|
||||
</div>
|
||||
|
||||
<br></br>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<div style={searchRequestCSS.body}>
|
||||
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
|
||||
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
|
||||
</div>
|
||||
|
||||
<div className='box' style={searchRequestCSS.box}>
|
||||
<div style={searchRequestCSS.searchSmallContainer}>
|
||||
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||
|
||||
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
|
||||
<br></br><br></br>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
||||
|
||||
// <form style={searchRequestCSS.controls}>
|
||||
// <label style={searchRequestCSS.withData}>
|
||||
// <div style={searchRequestCSS.sortOptions}>Discover</div>
|
||||
// </label>
|
||||
// </form>
|
||||
|
||||
// <form style={searchRequestCSS.controls}>
|
||||
// <label style={searchRequestCSS.withData}>
|
||||
// <select style={searchRequestCSS.sortOptions}>
|
||||
// <option value="discover">All</option>
|
||||
// <option value="nowplaying">Movies</option>
|
||||
// <option value="nowplaying">TV Shows</option>
|
||||
// </select>
|
||||
// </label>
|
||||
// </form>
|
||||
}
|
||||
|
||||
export default SearchRequest;
|
||||
92
.archive/client/app/components/admin/Admin.jsx
Normal file
92
.archive/client/app/components/admin/Admin.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import LoginForm from './LoginForm/LoginForm.jsx';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from '../redux/store.jsx';
|
||||
|
||||
import { getCookie } from '../Cookie.jsx';
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import Sidebar from './Sidebar.jsx';
|
||||
import AdminRequestInfo from './AdminRequestInfo.jsx';
|
||||
|
||||
import adminCSS from '../styles/adminComponent.jsx'
|
||||
|
||||
|
||||
class AdminComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
requested_objects: '',
|
||||
}
|
||||
|
||||
this.updateHandler = this.updateHandler.bind(this)
|
||||
}
|
||||
|
||||
// Fetches all requested elements and updates the state with response
|
||||
componentWillMount() {
|
||||
this.fetchRequestedItems()
|
||||
}
|
||||
|
||||
fetchRequestedItems() {
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
|
||||
.then(result => {
|
||||
this.setState({
|
||||
requested_objects: result.results.reverse()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateHandler() {
|
||||
this.fetchRequestedItems()
|
||||
}
|
||||
|
||||
// Displays loginform if not logged in and passes response from
|
||||
// api call to sidebar and infoPanel through props
|
||||
verifyLoggedIn() {
|
||||
const logged_in = getCookie('logged_in');
|
||||
if (!logged_in) {
|
||||
return <LoginForm />
|
||||
}
|
||||
|
||||
let selectedRequest = undefined;
|
||||
let listItemSelected = undefined;
|
||||
|
||||
const requestParam = this.props.match.params.request;
|
||||
|
||||
if (requestParam && this.state.requested_objects !== '') {
|
||||
selectedRequest = this.state.requested_objects[requestParam]
|
||||
listItemSelected = requestParam;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={adminCSS.selectedObjectPanel}>
|
||||
<AdminRequestInfo
|
||||
selectedRequest={selectedRequest}
|
||||
listItemSelected={listItemSelected}
|
||||
updateHandler = {this.updateHandler}
|
||||
/>
|
||||
</div>
|
||||
<div style={adminCSS.sidebar}>
|
||||
<Sidebar
|
||||
requested_objects={this.state.requested_objects}
|
||||
listItemSelected={listItemSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{ this.verifyLoggedIn() }
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AdminComponent;
|
||||
218
.archive/client/app/components/admin/AdminRequestInfo.jsx
Normal file
218
.archive/client/app/components/admin/AdminRequestInfo.jsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import PirateSearch from './PirateSearch.jsx'
|
||||
// No in use!
|
||||
import InfoButton from '../buttons/InfoButton.jsx';
|
||||
|
||||
// Stylesheets
|
||||
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
|
||||
import buttonsCSS from '../styles/buttons.jsx';
|
||||
|
||||
|
||||
String.prototype.capitalize = function() {
|
||||
return this.charAt(0).toUpperCase() + this.slice(1);
|
||||
}
|
||||
|
||||
|
||||
class AdminRequestInfo extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
statusValue: '',
|
||||
movieInfo: undefined,
|
||||
expandedSummary: false,
|
||||
}
|
||||
|
||||
this.requestInfo = '';
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.requestInfo = props.selectedRequest;
|
||||
this.state.statusValue = this.requestInfo.status;
|
||||
this.state.expandedSummary = false;
|
||||
this.fetchIteminfo()
|
||||
}
|
||||
|
||||
userAgent(agent) {
|
||||
if (agent) {
|
||||
try {
|
||||
return agent.split(" ")[1].replace(/[\(\;]/g, '');
|
||||
}
|
||||
catch(e) {
|
||||
return agent;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
generateStatusDropdown() {
|
||||
return (
|
||||
<select onChange={ event => this.updateRequestStatus(event) } value={this.state.statusValue}>
|
||||
<option value='requested'>Requested</option>
|
||||
<option value='downloading'>Downloading</option>
|
||||
<option value='downloaded'>Downloaded</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
updateRequestStatus(event) {
|
||||
const eventValue = event.target.value;
|
||||
const itemID = this.requestInfo.id;
|
||||
|
||||
const apiData = {
|
||||
type: this.requestInfo.type,
|
||||
status: eventValue,
|
||||
}
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + itemID, 'PUT', apiData)
|
||||
.then((response) => {
|
||||
console.log('Response, updateRequestStatus: ', response)
|
||||
this.props.updateHandler()
|
||||
})
|
||||
}
|
||||
|
||||
generateStatusIndicator(status) {
|
||||
switch (status) {
|
||||
case 'requested':
|
||||
// Yellow
|
||||
return 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
case 'downloading':
|
||||
// Blue
|
||||
return 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
case 'downloaded':
|
||||
// Green
|
||||
return 'linear-gradient(to right, #39aa56 0, #39aa56 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
default:
|
||||
return 'linear-gradient(to right, grey 0, grey 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
}
|
||||
}
|
||||
|
||||
generateTypeIcon(type) {
|
||||
if (type === 'show')
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
|
||||
)
|
||||
else if (type === 'movie')
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line></svg>
|
||||
)
|
||||
}
|
||||
|
||||
toggleSummmaryLength() {
|
||||
this.setState({
|
||||
expandedSummary: !this.state.expandedSummary
|
||||
})
|
||||
}
|
||||
|
||||
generateSummary() {
|
||||
// { this.state.movieInfo != undefined ? this.state.movieInfo.summary : 'Loading...' }
|
||||
const info = this.state.movieInfo;
|
||||
if (info !== undefined) {
|
||||
const summary = this.state.movieInfo.summary
|
||||
const summary_short = summary.slice(0, 180);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span><b>Matched: </b> {String(info.matchedInPlex)}</span> <br/>
|
||||
<span><b>Rating: </b> {info.rating}</span> <br/>
|
||||
<span><b>Popularity: </b> {info.popularity}</span> <br/>
|
||||
{
|
||||
(summary.length > 180 && this.state.expandedSummary === false) ?
|
||||
<span><b>Summary: </b> { summary_short }<span onClick = {() => this.toggleSummmaryLength()}>... <span style={{color: 'blue', cursor: 'pointer'}}>Show more</span></span></span>
|
||||
:
|
||||
<span><b>Summary: </b> { summary }<span onClick = {() => this.toggleSummmaryLength()}><span style={{color: 'blue', cursor: 'pointer'}}> Show less</span></span></span>
|
||||
|
||||
}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <span>Loading...</span>
|
||||
}
|
||||
}
|
||||
|
||||
requested_by_user(request_user) {
|
||||
if (request_user === 'NULL')
|
||||
return undefined
|
||||
|
||||
return (
|
||||
<span><b>Requested by:</b> {request_user}</span>
|
||||
)
|
||||
}
|
||||
|
||||
fetchIteminfo() {
|
||||
const itemID = this.requestInfo.id;
|
||||
const type = this.requestInfo.type;
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/tmdb/' + itemID +'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
console.log('Response, getInfo:', response)
|
||||
this.setState({
|
||||
movieInfo: response
|
||||
});
|
||||
console.log(this.state.movieInfo)
|
||||
})
|
||||
}
|
||||
|
||||
displayInfo() {
|
||||
const request = this.props.selectedRequest;
|
||||
|
||||
if (request) {
|
||||
requestInfoCSS.info.background = this.generateStatusIndicator(request.status);
|
||||
|
||||
return (
|
||||
<div style={requestInfoCSS.wrapper}>
|
||||
|
||||
<div style={requestInfoCSS.stick}>
|
||||
<span style={requestInfoCSS.title}> {request.title} {request.year}</span>
|
||||
<span style={{marginLeft: '2em'}}>
|
||||
<span style={requestInfoCSS.type_icon}>{this.generateTypeIcon(request.type)}</span>
|
||||
{/*<span style={style.type_text}>{request.type.capitalize()}</span> <br />*/}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info}>
|
||||
<div style={requestInfoCSS.info_poster}>
|
||||
<img src={'https://image.tmdb.org/t/p/w185' + request.poster_path} style={requestInfoCSS.image} alt='Movie poster image'></img>
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info_request}>
|
||||
<h3 style={requestInfoCSS.info_request_header}>Request info</h3>
|
||||
|
||||
<span><b>status:</b>{ request.status }</span><br />
|
||||
<span><b>ip:</b>{ request.ip }</span><br />
|
||||
<span><b>user_agent:</b>{ this.userAgent(request.user_agent) }</span><br />
|
||||
<span><b>request_date:</b>{ request.requested_date}</span><br />
|
||||
{ this.requested_by_user(request.requested_by) }<br />
|
||||
{ this.generateStatusDropdown() }<br />
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info_movie}>
|
||||
<h3 style={requestInfoCSS.info_movie_header}>Movie info</h3>
|
||||
|
||||
{ this.generateSummary() }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PirateSearch style={requestInfoCSS.search} name={request.title} />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>{this.displayInfo()}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminRequestInfo;
|
||||
66
.archive/client/app/components/admin/LoginForm/LoginForm.jsx
Normal file
66
.archive/client/app/components/admin/LoginForm/LoginForm.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { login } from '../../redux/reducer.jsx';
|
||||
|
||||
class LoginForm extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {email, password} = this.state;
|
||||
let {isLoginPending, isLoginSuccess, loginError} = this.props;
|
||||
return (
|
||||
<form name="loginForm" onSubmit={this.onSubmit}>
|
||||
<div className="form-group-collection">
|
||||
<div className="form-group">
|
||||
<label>Email:</label>
|
||||
<input type="" name="email" onChange={e => this.setState({email: e.target.value})} value={email}/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" onChange={e => this.setState({password: e.target.value})} value={password}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Login" />
|
||||
|
||||
<div className="message">
|
||||
{ isLoginPending && <div>Please wait...</div> }
|
||||
{ isLoginSuccess && <div>Success.</div> }
|
||||
{ loginError && <div>{loginError.message}</div> }
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
onSubmit(e) {
|
||||
e.preventDefault();
|
||||
let { email, password } = this.state;
|
||||
this.props.login(email, password);
|
||||
this.setState({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
isLoginPending: state.isLoginPending,
|
||||
isLoginSuccess: state.isLoginSuccess,
|
||||
loginError: state.loginError
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
login: (email, password) => dispatch(login(email, password))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
|
||||
95
.archive/client/app/components/admin/PirateSearch.jsx
Normal file
95
.archive/client/app/components/admin/PirateSearch.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { Component } from 'react';
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
// Components
|
||||
import TorrentTable from './TorrentTable.jsx'
|
||||
|
||||
// Stylesheets
|
||||
import btnStylesheet from '../styles/buttons.jsx';
|
||||
|
||||
// Interactive button
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import Loading from '../images/loading.jsx'
|
||||
|
||||
class PirateSearch extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
torrentResponse: undefined,
|
||||
name: '',
|
||||
loading: null,
|
||||
showButton: true,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.name != this.state.name) {
|
||||
this.setState({
|
||||
torrentResponse: undefined,
|
||||
showButton: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
searchTheBay() {
|
||||
const query = this.props.name;
|
||||
const type = this.props.type;
|
||||
|
||||
this.setState({
|
||||
showButton: false,
|
||||
loading: <Loading />,
|
||||
})
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
// fetchJSON('http://localhost:31459/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
console.log('this is the first response: ', response)
|
||||
if (response.success === true) {
|
||||
this.setState({
|
||||
torrentResponse: response.torrents,
|
||||
loading: null,
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error(response.message)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.setState({
|
||||
showButton: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
btnStylesheet.submit.top = '50%'
|
||||
btnStylesheet.submit.position = 'absolute'
|
||||
btnStylesheet.submit.marginLeft = '-75px'
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.state.showButton ?
|
||||
<div style={{textAlign:'center'}}>
|
||||
<Interactive
|
||||
as='button'
|
||||
onClick={() => {this.searchTheBay()}}
|
||||
style={btnStylesheet.submit}
|
||||
focus={btnStylesheet.submit_hover}
|
||||
hover={btnStylesheet.submit_hover}>
|
||||
|
||||
<span style={{whiteSpace: 'nowrap'}}>Search for torrents</span>
|
||||
</Interactive>
|
||||
</div>
|
||||
: null }
|
||||
|
||||
{ this.state.loading }
|
||||
|
||||
<TorrentTable response={this.state.torrentResponse} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PirateSearch
|
||||
247
.archive/client/app/components/admin/Sidebar.jsx
Normal file
247
.archive/client/app/components/admin/Sidebar.jsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import sidebarCSS from '../styles/adminSidebar.jsx'
|
||||
|
||||
class SidebarComponent extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
// Constructor with states holding the search query and the element of reponse.
|
||||
this.state = {
|
||||
filterValue: '',
|
||||
filterQuery: '',
|
||||
requestItemsToBeDisplayed: [],
|
||||
listItemSelected: '',
|
||||
height: '0',
|
||||
}
|
||||
|
||||
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
|
||||
}
|
||||
|
||||
// Where we wait for api response to be delivered from parent through props
|
||||
componentWillReceiveProps(props) {
|
||||
this.state.listItemSelected = props.listItemSelected;
|
||||
this.displayRequestedElementsInfo(props.requested_objects);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateWindowDimensions();
|
||||
window.addEventListener('resize', this.updateWindowDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateWindowDimensions);
|
||||
}
|
||||
|
||||
updateWindowDimensions() {
|
||||
this.setState({ height: window.innerHeight });
|
||||
}
|
||||
|
||||
// Inputs a date and returns a text string that matches how long it was since
|
||||
convertDateToDaysSince(date) {
|
||||
var oneDay = 24*60*60*1000;
|
||||
var firstDate = new Date(date);
|
||||
var secondDate = new Date();
|
||||
|
||||
var diffDays = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay));
|
||||
|
||||
switch (diffDays) {
|
||||
case 0:
|
||||
return 'Today';
|
||||
case 1:
|
||||
return '1 day ago'
|
||||
default:
|
||||
return diffDays + ' days ago'
|
||||
}
|
||||
}
|
||||
|
||||
// Called from our dropdown, receives a filter string and checks it with status field
|
||||
// of our request objects.
|
||||
filterItems(filterValue) {
|
||||
let filteredRequestElements = this.props.requested_objects.map((item, index) => {
|
||||
if (item.status === filterValue || filterValue === 'all')
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: filteredRequestElements,
|
||||
filterValue: filterValue,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Updates the internal state of the query filter and updates the list to only
|
||||
// display names matching the query. This is real-time filtering.
|
||||
updateFilterQuery(event) {
|
||||
const query = event.target.value;
|
||||
|
||||
let filteredByQuery = this.props.requested_objects.map((item, index) => {
|
||||
if (item.title.toLowerCase().indexOf(query.toLowerCase()) != -1)
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: filteredByQuery,
|
||||
filterQuery: query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
generateFilterSearch() {
|
||||
return (
|
||||
<div style={sidebarCSS.searchSidebar}>
|
||||
<div style={sidebarCSS.searchInner}>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
style={sidebarCSS.searchTextField}
|
||||
placeholder="Search requested items"
|
||||
onChange={event => this.updateFilterQuery(event)}
|
||||
value={this.state.filterQuery}/>
|
||||
<span>
|
||||
<svg id="icon-search" style={sidebarCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
|
||||
<g id="search">
|
||||
<circle style={sidebarCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
|
||||
<path style={sidebarCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
generateNav() {
|
||||
let filterValue = this.state.filterValue;
|
||||
|
||||
return (
|
||||
<nav style={sidebarCSS.sidebar_navbar_underline}>
|
||||
<ul style={sidebarCSS.ulFilterSelectors}>
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('all') }>All</span>
|
||||
{ (filterValue === 'all' || filterValue === '') && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('requested') }>Requested</span>
|
||||
{ filterValue === 'requested' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloading') }>Downloading</span>
|
||||
{ filterValue === 'downloading' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloaded') }>Downloaded</span>
|
||||
{ filterValue === 'downloaded' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
generateBody(cards) {
|
||||
let style = sidebarCSS.ulCard;
|
||||
style.maxHeight = this.state.height - 160;
|
||||
|
||||
return (
|
||||
<ul style={style}>
|
||||
{ cards }
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
generateListElements(index, item) {
|
||||
let statusBar;
|
||||
|
||||
switch (item.status) {
|
||||
case 'requested':
|
||||
// Yellow
|
||||
statusBar = { background: 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
case 'downloading':
|
||||
// Blue
|
||||
statusBar = { background: 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
case 'downloaded':
|
||||
// Green
|
||||
statusBar = { background: 'linear-gradient(to right, #39aa56 0, #39aa56 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
default:
|
||||
statusBar = { background: 'linear-gradient(to right, grey 0, grey 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
}
|
||||
|
||||
statusBar.listStyleType = 'none';
|
||||
|
||||
return (
|
||||
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}} key={index}>
|
||||
<li style={statusBar}>
|
||||
<Interactive
|
||||
as='div'
|
||||
style={ (index != this.state.listItemSelected) ? sidebarCSS.card : sidebarCSS.cardSelected }
|
||||
hover={sidebarCSS.cardSelected}
|
||||
focus={sidebarCSS.cardSelected}
|
||||
active={sidebarCSS.cardSelected}>
|
||||
|
||||
<h2 style={sidebarCSS.titleCard}>
|
||||
<span>{ item.title }</span>
|
||||
</h2>
|
||||
|
||||
<p style={sidebarCSS.pCard}>
|
||||
<span>Requested:
|
||||
<time>
|
||||
{ this.convertDateToDaysSince(item.requested_date) }
|
||||
</time>
|
||||
</span>
|
||||
</p>
|
||||
</Interactive>
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// This is our main loader that gets called when we receive api response through props from parent
|
||||
displayRequestedElementsInfo(requested_objects) {
|
||||
let requestedElement = requested_objects.map((item, index) => {
|
||||
if (['requested', 'downloading', 'downloaded'].indexOf(this.state.filterValue) != -1) {
|
||||
if (item.status === this.state.filterValue){
|
||||
return this.generateListElements(index, item);
|
||||
}
|
||||
}
|
||||
else if (this.state.filterQuery !== '') {
|
||||
if (item.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) != -1)
|
||||
return this.generateListElements(index, item);
|
||||
}
|
||||
else
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: this.generateBody(requestedElement)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
// if (typeof InstallTrigger !== 'undefined')
|
||||
// bodyCSS.width = '-moz-min-content';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={sidebarCSS.header}>Requested items</h1>
|
||||
{ this.generateFilterSearch() }
|
||||
{ this.generateNav() }
|
||||
|
||||
<div key='requestedTable' style={sidebarCSS.body}>
|
||||
{ this.state.requestItemsToBeDisplayed }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarComponent;
|
||||
209
.archive/client/app/components/admin/TorrentTable.jsx
Normal file
209
.archive/client/app/components/admin/TorrentTable.jsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import torrentTableCSS from '../styles/adminTorrentTable.jsx';
|
||||
|
||||
class TorrentTable extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
torrentResponse: [],
|
||||
listElements: undefined,
|
||||
showTable: false,
|
||||
filterQuery: '',
|
||||
sortValue: 'name',
|
||||
sortDesc: true,
|
||||
}
|
||||
|
||||
this.UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.response !== undefined && props.response !== this.state.torrentResponse) {
|
||||
console.log('not called', props)
|
||||
this.setState({
|
||||
torrentResponse: props.response,
|
||||
showTable: true,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
showTable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// BORROWED FROM GITHUB user sindresorhus
|
||||
// Link to repo: https://github.com/sindresorhus/pretty-bytes
|
||||
convertSizeToHumanSize(num) {
|
||||
if (!Number.isFinite(num)) {
|
||||
return num
|
||||
// throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
|
||||
}
|
||||
const neg = num < 0;
|
||||
|
||||
if (neg) {
|
||||
num = -num;
|
||||
}
|
||||
|
||||
if (num < 1) {
|
||||
return (neg ? '-' : '') + num + ' B';
|
||||
}
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log10(num) / 3), this.UNITS.length - 1);
|
||||
const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
|
||||
const unit = this.UNITS[exponent];
|
||||
|
||||
return (neg ? '-' : '') + numStr + ' ' + unit;
|
||||
}
|
||||
|
||||
convertHumanSizeToBytes(string) {
|
||||
const [numStr, unit] = string.split(' ');
|
||||
if (this.UNITS.indexOf(unit) === -1) {
|
||||
return string
|
||||
}
|
||||
|
||||
const exponent = this.UNITS.indexOf(unit) * 3
|
||||
|
||||
return numStr * (Math.pow(10, exponent))
|
||||
}
|
||||
|
||||
sendToDownload(magnet) {
|
||||
const apiData = {
|
||||
magnet: magnet,
|
||||
}
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', apiData)
|
||||
// fetchJSON('http://localhost:31459/api/v1/pirate/add', 'POST', apiData)
|
||||
.then((response) => {
|
||||
console.log('Response, addMagnet: ', response)
|
||||
// TODO Display the feedback in a notification component (text, status)
|
||||
})
|
||||
}
|
||||
|
||||
// Updates the internal state of the query filter and updates the list to only
|
||||
// display names matching the query. This is real-time filtering.
|
||||
updateFilterQuery(event) {
|
||||
const query = event.target.value;
|
||||
|
||||
let filteredByQuery = this.props.response.map((item, index) => {
|
||||
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
|
||||
return item
|
||||
})
|
||||
|
||||
this.setState({
|
||||
torrentResponse: filteredByQuery,
|
||||
filterQuery: query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
sortTable(col) {
|
||||
let direction = this.state.sortDesc;
|
||||
if (col === this.state.sortValue)
|
||||
direction = !direction;
|
||||
else
|
||||
direction = true
|
||||
|
||||
let sortedItems = this.state.torrentResponse.sort((a,b) => {
|
||||
// This is so we also can sort string that only contain numbers
|
||||
let valueA = isNaN(a[col]) ? a[col] : parseInt(a[col])
|
||||
let valueB = isNaN(b[col]) ? b[col] : parseInt(b[col])
|
||||
|
||||
valueA = (col == 'size') ? this.convertHumanSizeToBytes(valueA) : valueA
|
||||
valueB = (col == 'size') ? this.convertHumanSizeToBytes(valueB) : valueB
|
||||
|
||||
if (direction)
|
||||
return valueA<valueB? 1:valueA>valueB?-1:0;
|
||||
else
|
||||
return valueA>valueB? 1:valueA<valueB?-1:0;
|
||||
})
|
||||
|
||||
this.setState({
|
||||
torrentResponse: sortedItems,
|
||||
sortDesc: direction,
|
||||
sortValue: col,
|
||||
})
|
||||
}
|
||||
|
||||
generateFilterSearch() {
|
||||
return (
|
||||
<div style={torrentTableCSS.searchSidebar}>
|
||||
<div style={torrentTableCSS.searchInner}>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
style={torrentTableCSS.searchTextField}
|
||||
placeholder="Filter torrents by query"
|
||||
onChange={event => this.updateFilterQuery(event)}
|
||||
value={this.state.filterQuery}/>
|
||||
<span>
|
||||
<svg id="icon-search" style={torrentTableCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
|
||||
<g id="search">
|
||||
<circle style={torrentTableCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
|
||||
<path style={torrentTableCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
generateListElements() {
|
||||
let listElements = this.state.torrentResponse.map((item, index) => {
|
||||
if (item !== undefined) {
|
||||
let title = item.name
|
||||
let size = this.convertSizeToHumanSize(item.size)
|
||||
|
||||
return (
|
||||
<tr key={index} style={torrentTableCSS.bodyCol}>
|
||||
<td>{ item.name }</td>
|
||||
<td>{ item.uploader }</td>
|
||||
<td>{ size }</td>
|
||||
<td>{ item.seed }</td>
|
||||
<td><button onClick = { event => this.sendToDownload(item.magnet) }>Send to download</button></td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
})
|
||||
return listElements
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style= { this.state.showTable ? null : {display: 'none'}}>
|
||||
{ this.generateFilterSearch() }
|
||||
<table style={torrentTableCSS.table} cellSpacing="0" cellPadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('name') }>
|
||||
Title
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'name' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('uploader') }>
|
||||
Uploader
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'uploader' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('size') }>
|
||||
Size
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'size' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('seed') }>
|
||||
Seeds
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'seed' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col}>Magnet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.generateListElements()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TorrentTable;
|
||||
52
.archive/client/app/components/buttons/InfoButton.jsx
Normal file
52
.archive/client/app/components/buttons/InfoButton.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { Component } from 'react';
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import buttonsCSS from '../styles/buttons.jsx';
|
||||
|
||||
|
||||
class InfoButton extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props) {
|
||||
this.state = {
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setState({
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
})
|
||||
}
|
||||
|
||||
getTMDBLink() {
|
||||
const id = this.state.id;
|
||||
const type = this.state.type;
|
||||
|
||||
if (type === 'movie')
|
||||
return 'https://www.themoviedb.org/movie/' + id
|
||||
else if (type === 'show')
|
||||
return 'https://www.themoviedb.org/tv/' + id
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<a href={this.getTMDBLink()}>
|
||||
<Interactive
|
||||
as='button'
|
||||
hover={buttonsCSS.info_hover}
|
||||
focus={buttonsCSS.info_hover}
|
||||
style={buttonsCSS.info}>
|
||||
|
||||
<span>More info</span>
|
||||
</Interactive>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InfoButton;
|
||||
22
.archive/client/app/components/buttons/request_button.jsx
Normal file
22
.archive/client/app/components/buttons/request_button.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
class RequestButton extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {textColor: 'white'};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Text
|
||||
style={{color: this.state.textColor}}
|
||||
onEnter={() => this.setState({textColor: 'red'})}
|
||||
onExit={() => this.setState({textColor: 'white'})}>
|
||||
This text will turn red when you look at it.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RequestButton;
|
||||
53
.archive/client/app/components/http.jsx
Normal file
53
.archive/client/app/components/http.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
// class http {
|
||||
// dispatch(obj) {
|
||||
// console.log(obj);
|
||||
// }
|
||||
|
||||
function checkStatus(response) {
|
||||
const hasError = (response.status < 200 || response.status >= 300)
|
||||
if (hasError) {
|
||||
throw response.text();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
function parseJSON(response) { return response.json(); }
|
||||
|
||||
|
||||
|
||||
// *
|
||||
// * Retrieve search results from tmdb with added seasoned information.
|
||||
// * @param {String} uri query you want to search for
|
||||
// * @param {Number} page representing pagination of results
|
||||
// * @returns {Promise} succeeds if results were found
|
||||
|
||||
// fetchSearch(uri) {
|
||||
// fetch(uri, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'authorization': getCookie('token')
|
||||
// },
|
||||
// })
|
||||
// .then(response => {
|
||||
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// export default http;
|
||||
|
||||
export function fetchJSON(url, method, data) {
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'authorization': getCookie('token'),
|
||||
'loggedinuser': getCookie('loggedInUser'),
|
||||
}),
|
||||
body: JSON.stringify(data)
|
||||
}).then(checkStatus).then(parseJSON);
|
||||
}
|
||||
34
.archive/client/app/components/images/loading.jsx
Normal file
34
.archive/client/app/components/images/loading.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<svg version="1.1"
|
||||
style={{height: '75px'}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 80 80">
|
||||
<path
|
||||
fill="#e9a131"
|
||||
d="M40,72C22.4,72,8,57.6,8,40C8,22.4,
|
||||
22.4,8,40,8c17.6,0,32,14.4,32,32c0,1.1-0.9,2-2,2
|
||||
s-2-0.9-2-2c0-15.4-12.6-28-28-28S12,24.6,12,40s12.6,
|
||||
28,28,28c1.1,0,2,0.9,2,2S41.1,72,40,72z">
|
||||
|
||||
<animateTransform
|
||||
attributeType="xml"
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 40 40"
|
||||
to="360 40 40"
|
||||
dur="1.0s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
109
.archive/client/app/components/redux/reducer.jsx
Normal file
109
.archive/client/app/components/redux/reducer.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
import { setCookie } from '../Cookie.jsx';
|
||||
|
||||
const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING';
|
||||
const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS';
|
||||
const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR';
|
||||
|
||||
export function login(email, password) {
|
||||
return dispatch => {
|
||||
dispatch(setLoginPending(true));
|
||||
dispatch(setLoginSuccess(false));
|
||||
dispatch(setLoginError(null));
|
||||
|
||||
callLoginApi(email, password, error => {
|
||||
dispatch(setLoginPending(false));
|
||||
if (!error) {
|
||||
dispatch(setLoginSuccess(true));
|
||||
} else {
|
||||
dispatch(setLoginError(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginPending(isLoginPending) {
|
||||
return {
|
||||
type: SET_LOGIN_PENDING,
|
||||
isLoginPending
|
||||
};
|
||||
}
|
||||
|
||||
function setLoginSuccess(isLoginSuccess) {
|
||||
return {
|
||||
type: SET_LOGIN_SUCCESS,
|
||||
isLoginSuccess
|
||||
};
|
||||
}
|
||||
|
||||
function setLoginError(loginError) {
|
||||
return {
|
||||
type: SET_LOGIN_ERROR,
|
||||
loginError
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function callLoginApi(username, password, callback) {
|
||||
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/user/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
response.json()
|
||||
.then((data) => {
|
||||
if (data.success === true) {
|
||||
let token = data.token;
|
||||
setCookie('token', token, 10);
|
||||
setCookie('logged_in', true, 10);
|
||||
setCookie('loggedInUser', username, 10);
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
return callback(null);
|
||||
})
|
||||
|
||||
case 401:
|
||||
return callback(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
return callback(new Error('Invalid username and password'));
|
||||
});
|
||||
}
|
||||
|
||||
export default function reducer(state = {
|
||||
isLoginSuccess: false,
|
||||
isLoginPending: false,
|
||||
loginError: null
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case SET_LOGIN_PENDING:
|
||||
return Object.assign({}, state, {
|
||||
isLoginPending: action.isLoginPending
|
||||
});
|
||||
|
||||
case SET_LOGIN_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isLoginSuccess: action.isLoginSuccess
|
||||
});
|
||||
|
||||
case SET_LOGIN_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loginError: action.loginError
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
7
.archive/client/app/components/redux/store.jsx
Normal file
7
.archive/client/app/components/redux/store.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import logger from 'redux-logger';
|
||||
import reducer from './reducer.jsx';
|
||||
|
||||
const store = createStore(reducer, {}, applyMiddleware(thunk, logger));
|
||||
export default store;
|
||||
16
.archive/client/app/components/styles/adminComponent.jsx
Normal file
16
.archive/client/app/components/styles/adminComponent.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
float: 'left',
|
||||
width: '18%',
|
||||
minWidth: '250px',
|
||||
fontFamily: '"Open Sans", sans-serif',
|
||||
fontSize: '14px',
|
||||
borderRight: '2px solid #f2f2f2',
|
||||
},
|
||||
selectedObjectPanel: {
|
||||
width: '80%',
|
||||
float: 'right',
|
||||
fontFamily: '"Open Sans", sans-serif',
|
||||
marginTop: '1em',
|
||||
}
|
||||
}
|
||||
58
.archive/client/app/components/styles/adminRequestInfo.jsx
Normal file
58
.archive/client/app/components/styles/adminRequestInfo.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
export default {
|
||||
wrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
stick: {
|
||||
marginBottom: '1em',
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: '2em',
|
||||
},
|
||||
image: {
|
||||
width: '105px',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
|
||||
info: {
|
||||
paddingTop: '1em',
|
||||
paddingBottom: '0.5em',
|
||||
marginRight: '2em',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
},
|
||||
|
||||
type_icon: {
|
||||
marginLeft: '-0.2em',
|
||||
marginRight: '0.7em',
|
||||
},
|
||||
type_text: {
|
||||
verticalAlign: 'super',
|
||||
},
|
||||
|
||||
|
||||
info_poster: {
|
||||
marginLeft: '2em',
|
||||
flex: '0 1 10%'
|
||||
},
|
||||
|
||||
info_request: {
|
||||
flex: '0 1 auto'
|
||||
},
|
||||
info_request_header: {
|
||||
margin: '0',
|
||||
marginBottom: '0.5em',
|
||||
},
|
||||
|
||||
info_movie: {
|
||||
maxWidth: '70%',
|
||||
marginLeft: '1em',
|
||||
flex: '0 1 auto',
|
||||
},
|
||||
info_movie_header: {
|
||||
margin: '0',
|
||||
marginBottom: '0.5em',
|
||||
}
|
||||
}
|
||||
153
.archive/client/app/components/styles/adminSidebar.jsx
Normal file
153
.archive/client/app/components/styles/adminSidebar.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
export default {
|
||||
header: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
parentElement: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '2px',
|
||||
padding: '4px',
|
||||
margin: '4px',
|
||||
marginLeft: '4px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
parentElement_hover: {
|
||||
backgroundColor: '#f8f8f8',
|
||||
pointer: 'hand',
|
||||
},
|
||||
|
||||
parentElement_active: {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
|
||||
parentElement_selected: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '2px',
|
||||
padding: '4px',
|
||||
margin: '4px 0px 4px 4px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
title: {
|
||||
maxWidth: '65%',
|
||||
display: 'inline-flex',
|
||||
},
|
||||
|
||||
link: {
|
||||
color: 'black',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
|
||||
rightContainer: {
|
||||
float: 'right',
|
||||
},
|
||||
|
||||
|
||||
|
||||
searchSidebar: {
|
||||
height: '4em',
|
||||
},
|
||||
searchInner: {
|
||||
top: '0',
|
||||
right: '0',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
width: '90%',
|
||||
minWidth: '280px',
|
||||
height: '30px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchTextField: {
|
||||
display: 'inline-block',
|
||||
width: '90%',
|
||||
padding: '.3em',
|
||||
verticalAlign: 'middle',
|
||||
border: 'none',
|
||||
background: '#fff',
|
||||
fontSize: '14px',
|
||||
marginTop: '-7px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: '15px',
|
||||
height: '16px',
|
||||
marginRight: '4px',
|
||||
marginTop: '7px',
|
||||
},
|
||||
searchSVGIcon: {
|
||||
fill: 'none',
|
||||
stroke: '#9d9d9d',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: '10',
|
||||
},
|
||||
|
||||
|
||||
ulFilterSelectors: {
|
||||
borderBottom: '2px solid #f1f1f1',
|
||||
display: 'flex',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
listStyle: 'none',
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
aFilterSelectors: {
|
||||
color: '#3eaaaf',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
spanFilterSelectors: {
|
||||
content: '""',
|
||||
bottom: '-2px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
backgroundColor: '#3eaaaa',
|
||||
},
|
||||
|
||||
|
||||
ulCard: {
|
||||
margin: '1em 0 0 0',
|
||||
padding: '0',
|
||||
listStyle: 'none',
|
||||
borderBottom: '.46rem solid #f1f1f',
|
||||
backgroundColor: '#f1f1f1',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
|
||||
|
||||
card: {
|
||||
padding: '.1em .5em .8em 1.5em',
|
||||
marginBottom: '.26rem',
|
||||
height: 'auto',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
cardSelected: {
|
||||
padding: '.1em .5em .8em 1.5em',
|
||||
marginBottom: '.26rem',
|
||||
height: 'auto',
|
||||
cursor: 'pointer',
|
||||
|
||||
backgroundColor: '#f9f9f9',
|
||||
},
|
||||
titleCard: {
|
||||
fontSize: '15px',
|
||||
fontWeight: '400',
|
||||
whiteSpace: 'no-wrap',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
pCard: {
|
||||
margin: '0',
|
||||
},
|
||||
}
|
||||
59
.archive/client/app/components/styles/adminTorrentTable.jsx
Normal file
59
.archive/client/app/components/styles/adminTorrentTable.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
export default {
|
||||
table: {
|
||||
width: '80%',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
tableHeader: {
|
||||
},
|
||||
col: {
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
paddingBottom: '0.5em',
|
||||
textAlign: 'left',
|
||||
},
|
||||
bodyCol: {
|
||||
marginTop: '0.5em',
|
||||
},
|
||||
|
||||
searchSidebar: {
|
||||
height: '4em',
|
||||
marginTop: '1em',
|
||||
},
|
||||
searchInner: {
|
||||
top: '0',
|
||||
right: '0',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
width: '50%',
|
||||
minWidth: '280px',
|
||||
height: '30px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchTextField: {
|
||||
display: 'inline-block',
|
||||
width: '95%',
|
||||
padding: '.3em',
|
||||
verticalAlign: 'middle',
|
||||
border: 'none',
|
||||
background: '#fff',
|
||||
fontSize: '14px',
|
||||
marginTop: '-7px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: '15px',
|
||||
height: '16px',
|
||||
marginRight: '4px',
|
||||
marginTop: '7px',
|
||||
},
|
||||
searchSVGIcon: {
|
||||
fill: 'none',
|
||||
stroke: '#9d9d9d',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: '10',
|
||||
},
|
||||
}
|
||||
80
.archive/client/app/components/styles/buttons.jsx
Normal file
80
.archive/client/app/components/styles/buttons.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
export default {
|
||||
|
||||
submit: {
|
||||
color: '#e9a131',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#e9a131 2px solid',
|
||||
borderColor: '#e9a131',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
submit_hover: {
|
||||
backgroundColor: '#e9a131',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
info: {
|
||||
color: '#00d17c',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#00d17c 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
info_hover: {
|
||||
backgroundColor: '#00d17c',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
edit: {
|
||||
color: '#4a95da',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#4a95da 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
edit_small: {
|
||||
color: '#4a95da',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#4a95da 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '4px',
|
||||
minWidth: '50px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
edit_hover: {
|
||||
backgroundColor: '#4a95da',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
export default {
|
||||
bodyDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexFlow: 'row wrap',
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
|
||||
wrappingDiv: {
|
||||
|
||||
},
|
||||
|
||||
requestPoster: {
|
||||
height: '150px',
|
||||
},
|
||||
|
||||
infoDiv: {
|
||||
marginTop: 0,
|
||||
marginLeft: '10px',
|
||||
float: 'right',
|
||||
},
|
||||
}
|
||||
62
.archive/client/app/components/styles/searchObject.jsx
Normal file
62
.archive/client/app/components/styles/searchObject.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
export default {
|
||||
container: {
|
||||
maxWidth: '95%',
|
||||
margin: '0 auto',
|
||||
minHeight: '230px'
|
||||
},
|
||||
|
||||
title_large: {
|
||||
color: 'black',
|
||||
fontSize: '2em',
|
||||
},
|
||||
|
||||
title_small: {
|
||||
color: 'black',
|
||||
fontSize: '22px',
|
||||
},
|
||||
|
||||
stats_large: {
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
stats_small: {
|
||||
marginTop: '5px',
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
posterContainer: {
|
||||
float: 'left',
|
||||
zIndex: '3',
|
||||
position: 'relative',
|
||||
marginRight: '30px'
|
||||
},
|
||||
|
||||
posterImage: {
|
||||
border: '2px none',
|
||||
borderRadius: '2px',
|
||||
width: '150px'
|
||||
},
|
||||
|
||||
backgroundImage: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
buttons: {
|
||||
paddingTop: '20px',
|
||||
},
|
||||
|
||||
summary: {
|
||||
fontSize: '15px',
|
||||
},
|
||||
|
||||
dividerRow: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
itemDivider: {
|
||||
width: '90%',
|
||||
borderBottom: '1px solid grey',
|
||||
margin: '2rem auto'
|
||||
}
|
||||
}
|
||||
177
.archive/client/app/components/styles/searchRequestStyle.jsx
Normal file
177
.archive/client/app/components/styles/searchRequestStyle.jsx
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
export default {
|
||||
body: {
|
||||
fontFamily: "'Open Sans', sans-serif",
|
||||
backgroundColor: '#f7f7f7',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
minHeight: '100%',
|
||||
},
|
||||
|
||||
backgroundLargeHeader: {
|
||||
width: '100%',
|
||||
minHeight: '180px',
|
||||
backgroundColor: 'rgb(1, 28, 35)',
|
||||
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
|
||||
zIndex: 1,
|
||||
marginBottom: '70px'
|
||||
},
|
||||
|
||||
backgroundSmallHeader: {
|
||||
width: '100%',
|
||||
minHeight: '120px',
|
||||
backgroundColor: '#011c23',
|
||||
zIndex: 1,
|
||||
marginBottom: '40px'
|
||||
},
|
||||
|
||||
requestWrapper: {
|
||||
maxWidth: '1200px',
|
||||
margin: 'auto',
|
||||
paddingTop: '10px',
|
||||
backgroundColor: 'white',
|
||||
position: 'relative',
|
||||
zIndex: '10',
|
||||
boxShadow: '0 1px 2px grey',
|
||||
},
|
||||
|
||||
pageTitle: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
pageTitleLargeSpan: {
|
||||
color: 'white',
|
||||
fontSize: '3em',
|
||||
marginTop: '4vh',
|
||||
marginBottom: '6vh'
|
||||
},
|
||||
|
||||
pageTitleSmallSpan: {
|
||||
color: 'white',
|
||||
fontSize: '2em',
|
||||
marginTop: '3vh',
|
||||
marginBottom: '3vh'
|
||||
},
|
||||
|
||||
searchLargeContainer: {
|
||||
height: '52px',
|
||||
width: '77%',
|
||||
paddingLeft: '23%',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: 'grey 0px 1px 2px',
|
||||
},
|
||||
|
||||
searchSmallContainer: {
|
||||
},
|
||||
|
||||
searchIcon: {
|
||||
position: 'absolute',
|
||||
fontSize: '1.6em',
|
||||
marginTop: '7px',
|
||||
color: '#4f5b66',
|
||||
display: 'block',
|
||||
},
|
||||
|
||||
searchLargeBar: {
|
||||
width: '50%',
|
||||
height: '50px',
|
||||
background: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '12pt',
|
||||
float: 'left',
|
||||
color: '#63717f',
|
||||
paddingLeft: '40px',
|
||||
},
|
||||
|
||||
searchSmallBar: {
|
||||
width: '100%',
|
||||
height: '50px',
|
||||
background: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '11pt',
|
||||
float: 'left',
|
||||
color: '#63717f',
|
||||
paddingLeft: '65px',
|
||||
marginLeft: '-25px',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
|
||||
|
||||
// Dropdown for selecting tmdb lists
|
||||
controls: {
|
||||
textAlign: 'left',
|
||||
paddingTop: '8px',
|
||||
width: '33.3333%',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
},
|
||||
|
||||
withData: {
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '0',
|
||||
display: 'block',
|
||||
padding: '0',
|
||||
verticalAlign: 'baseline',
|
||||
font: 'inherit',
|
||||
textAlign: 'left',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
|
||||
sortOptions: {
|
||||
border: '1px solid #000',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
lineHeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: '4px 12px',
|
||||
paddingRight: '2rem',
|
||||
backgroundImage: 'url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOCAxOCI+CiAgPHRpdGxlPmFycm93LWRvd24tbWljcm88L3RpdGxlPgogIDxwb2x5bGluZSBwb2ludHM9IjE0IDQuNjcgOSAxMy4zMyA0IDQuNjciIHN0eWxlPSJmaWxsOiBub25lO3N0cm9rZTogIzAwMDtzdHJva2UtbWl0ZXJsaW1pdDogMTA7c3Ryb2tlLXdpZHRoOiAycHgiLz4KPC9zdmc+Cg==")',
|
||||
backgroundSize: '18px 18px',
|
||||
backgroundPosition: 'right 8px center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
width: 'auto',
|
||||
display: 'inline-block',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: '15px',
|
||||
WebkitAppearance: 'none',
|
||||
MozAppearance: 'none',
|
||||
appearance: 'none',
|
||||
},
|
||||
|
||||
|
||||
searchFilterActive: {
|
||||
color: '#00d17c',
|
||||
fontSize: '1em',
|
||||
marginLeft: '10px',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
searchFilterNotActive: {
|
||||
color: 'white',
|
||||
fontSize: '1em',
|
||||
marginLeft: '10px',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
filter: {
|
||||
color: 'white',
|
||||
paddingLeft: '40px',
|
||||
width: '60%',
|
||||
},
|
||||
|
||||
resultLargeHeader: {
|
||||
color: 'black',
|
||||
fontSize: '1.6em',
|
||||
width: '20%',
|
||||
},
|
||||
|
||||
resultSmallHeader: {
|
||||
paddingLeft: '12px',
|
||||
color: 'black',
|
||||
fontSize: '1.4em',
|
||||
},
|
||||
}
|
||||
15
.archive/client/app/index.html
Normal file
15
.archive/client/app/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
|
||||
<title>seasoned Shows</title>
|
||||
</head>
|
||||
<body style='margin: 0'>
|
||||
<div id="root">
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
20
.archive/client/app/index.js
Normal file
20
.archive/client/app/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 21:08:55
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-20 19:24:52
|
||||
|
||||
./client/index.js
|
||||
which is the webpack entry file
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import Root from './Root.jsx';
|
||||
|
||||
render((
|
||||
<HashRouter>
|
||||
<Root />
|
||||
</HashRouter>
|
||||
), document.getElementById('root'));
|
||||
44
.archive/client/package.json
Normal file
44
.archive/client/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "seasoned",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/KevinMidboe/seasonedShows",
|
||||
"author": "Kevin Midboe",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --open --config webpack.dev.js",
|
||||
"build": "NODE_ENV=production webpack --config webpack.prod.js",
|
||||
"build_dev": "webpack --config webpack.dev.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"css-loader": "^1.0.0",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"path": "^0.12.7",
|
||||
"react": "^15.6.1",
|
||||
"react-burger-menu": "^2.1.6",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-infinite-scroller": "^1.0.15",
|
||||
"react-interactive": "^0.8.1",
|
||||
"react-notify-toast": "^0.3.2",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-responsive": "^1.3.4",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"urijs": "^1.18.12",
|
||||
"webfontloader": "^1.6.28",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-dev-server": "^3.1.11",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1"
|
||||
}
|
||||
}
|
||||
33
.archive/client/webpack.common.js
Normal file
33
.archive/client/webpack.common.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-24 21:55:41
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './app/index.js',
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(['dist']),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './app/index.html',
|
||||
})
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
]
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
}
|
||||
};
|
||||
|
||||
17
.archive/client/webpack.dev.js
Normal file
17
.archive/client/webpack.dev.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-24 22:12:52
|
||||
*/
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'inline-source-map',
|
||||
devServer: {
|
||||
contentBase: './dist',
|
||||
headers: {'Access-Control-Allow-Origin': '*'}
|
||||
}
|
||||
});;
|
||||
28
.archive/client/webpack.prod.js
Normal file
28
.archive/client/webpack.prod.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-24 22:26:29
|
||||
*/
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const common = require('./webpack.common.js');
|
||||
var webpack = require('webpack')
|
||||
|
||||
module.exports = merge(common, {
|
||||
plugins: [
|
||||
new UglifyJSPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './app/index.html',
|
||||
title: 'Caching'
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
filename: '[name].[chunkhash].js',
|
||||
}
|
||||
});
|
||||
4
.archive/webpage/images/verified.svg
Normal file
4
.archive/webpage/images/verified.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" ?><svg id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
|
||||
.st0{fill:#2BB673;}
|
||||
.st1{fill:none;stroke:#FFFFFF;stroke-width:30;stroke-miterlimit:10;}
|
||||
</style><path class="st0" d="M489,255.9c0-0.2,0-0.5,0-0.7c0-1.6,0-3.2-0.1-4.7c0-0.9-0.1-1.8-0.1-2.8c0-0.9-0.1-1.8-0.1-2.7 c-0.1-1.1-0.1-2.2-0.2-3.3c0-0.7-0.1-1.4-0.1-2.1c-0.1-1.2-0.2-2.4-0.3-3.6c0-0.5-0.1-1.1-0.1-1.6c-0.1-1.3-0.3-2.6-0.4-4 c0-0.3-0.1-0.7-0.1-1C474.3,113.2,375.7,22.9,256,22.9S37.7,113.2,24.5,229.5c0,0.3-0.1,0.7-0.1,1c-0.1,1.3-0.3,2.6-0.4,4 c-0.1,0.5-0.1,1.1-0.1,1.6c-0.1,1.2-0.2,2.4-0.3,3.6c0,0.7-0.1,1.4-0.1,2.1c-0.1,1.1-0.1,2.2-0.2,3.3c0,0.9-0.1,1.8-0.1,2.7 c0,0.9-0.1,1.8-0.1,2.8c0,1.6-0.1,3.2-0.1,4.7c0,0.2,0,0.5,0,0.7c0,0,0,0,0,0.1s0,0,0,0.1c0,0.2,0,0.5,0,0.7c0,1.6,0,3.2,0.1,4.7 c0,0.9,0.1,1.8,0.1,2.8c0,0.9,0.1,1.8,0.1,2.7c0.1,1.1,0.1,2.2,0.2,3.3c0,0.7,0.1,1.4,0.1,2.1c0.1,1.2,0.2,2.4,0.3,3.6 c0,0.5,0.1,1.1,0.1,1.6c0.1,1.3,0.3,2.6,0.4,4c0,0.3,0.1,0.7,0.1,1C37.7,398.8,136.3,489.1,256,489.1s218.3-90.3,231.5-206.5 c0-0.3,0.1-0.7,0.1-1c0.1-1.3,0.3-2.6,0.4-4c0.1-0.5,0.1-1.1,0.1-1.6c0.1-1.2,0.2-2.4,0.3-3.6c0-0.7,0.1-1.4,0.1-2.1 c0.1-1.1,0.1-2.2,0.2-3.3c0-0.9,0.1-1.8,0.1-2.7c0-0.9,0.1-1.8,0.1-2.8c0-1.6,0.1-3.2,0.1-4.7c0-0.2,0-0.5,0-0.7 C489,256,489,256,489,255.9C489,256,489,256,489,255.9z" id="XMLID_3_"/><g id="XMLID_1_"><line class="st1" id="XMLID_2_" x1="213.6" x2="369.7" y1="344.2" y2="188.2"/><line class="st1" id="XMLID_4_" x1="233.8" x2="154.7" y1="345.2" y2="266.1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
88
.archive/webpage/js/main.js
Normal file
88
.archive/webpage/js/main.js
Normal file
@@ -0,0 +1,88 @@
|
||||
function getUrlParameter(name) {
|
||||
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
|
||||
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
|
||||
var results = regex.exec(location.search);
|
||||
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
|
||||
};
|
||||
|
||||
function getURLId() {
|
||||
var sPageURL = window.location.search.substring(1);
|
||||
var sParameterName = sPageURL.split('=');
|
||||
if (sParameterName[0] == 'id') {
|
||||
var query_id = document.getElementById('title').innerHTML = sParameterName[1];
|
||||
|
||||
return query_id;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
getShow();
|
||||
});
|
||||
|
||||
|
||||
// this is the id of the form
|
||||
$("#searchForm").submit(function(e) {
|
||||
var url = env_variables.url + 'verify/' + getUrlParameter('id');
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
success: function (data) {
|
||||
Materialize.toast('Episode successfully verified and moved!', 4000);
|
||||
},
|
||||
error: function(data) {
|
||||
Materialize.toast(data.responseJSON.error, 4000);
|
||||
console.log(data.responseJSON.error);
|
||||
e.preventDefault(); // avoid to execute the actual submit of the form.
|
||||
}
|
||||
});
|
||||
e.preventDefault(); // avoid to execute the actual submit of the form.
|
||||
});
|
||||
|
||||
|
||||
function foo(id) {
|
||||
console.log(id[0]);
|
||||
var el = $(id[0]);
|
||||
// if (el.attr('contenteditable') == 'true'){
|
||||
// el.attr('contenteditable', 'false');
|
||||
// } else {
|
||||
// el.attr('contenteditable', 'true')
|
||||
// }
|
||||
el.attr('contenteditable', 'true');
|
||||
}
|
||||
|
||||
function getShow() {
|
||||
var url = env_variables.url + getUrlParameter('id');
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
$('#parent').append('<br><span>' + data['parent'] + '</span>');
|
||||
if (data['verified']) {
|
||||
$('#verified').append('<img src="images/verified.svg">');
|
||||
}
|
||||
$('#name').append('<p>' + data['name'] + '</p>');
|
||||
$('#season').append('<p>' + data['season'] + '</p>');
|
||||
$('#episode').append('<p>' + data['episode'] + '</p>');
|
||||
var itemList= JSON.parse(data['video_files']);
|
||||
for (item in itemList) {
|
||||
$('#video_files').append('<p>' + itemList[item][0] + '</p>');
|
||||
$('#video_files').append('<p onclick="foo($(this));">' + itemList[item][1] + '</p>');
|
||||
}
|
||||
|
||||
var itemList= JSON.parse(data['subtitles']);
|
||||
for (item in itemList) {
|
||||
$('#subtitles').append('<p>' + itemList[item][0] + '</p>');
|
||||
$('#subtitles').append('<p onclick="foo($(this));">' + itemList[item][1] + '</p>');
|
||||
}
|
||||
|
||||
var itemList= JSON.parse(data['trash']);
|
||||
for (item in itemList) {
|
||||
$('#trash').append('<p>' + itemList[item] + '</p>');
|
||||
}
|
||||
console.log(data);
|
||||
},
|
||||
error: function(data) {
|
||||
console.log(data.responseJSON.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
32
.archive/webpage/style.css
Normal file
32
.archive/webpage/style.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-04-07 00:47:40
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-05-13 13:10:41
|
||||
*/
|
||||
|
||||
h3 {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
#verified {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#verified img {
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* valid color */
|
||||
.input-field input[type=text].valid {
|
||||
border-bottom: 1px solid #00a69a;
|
||||
box-shadow: 0 1px 0 0 #00a69a;
|
||||
}
|
||||
|
||||
/* invalid color */
|
||||
.input-field input[type=text].invalid {
|
||||
border-bottom: 1px solid #9e9e9e;
|
||||
box-shadow: 0 1px 0 0 #9e9e9e;
|
||||
}
|
||||
77
.archive/webpage/verify.html
Normal file
77
.archive/webpage/verify.html
Normal file
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>seasoned | verify</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css">
|
||||
|
||||
<!-- Compiled and minified JavaScript -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/js/materialize.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h3 id='title'></h3>
|
||||
|
||||
<div class="row formContainer">
|
||||
<form action="/" id="searchForm" class="col s12">
|
||||
<div class="row">
|
||||
<div class="input-field col s12">
|
||||
<span id='parent'>
|
||||
<label for="parent">Parent</label><br>
|
||||
</span>
|
||||
<span id='verified'>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col s12"></div>
|
||||
<div class="input-field col s4">
|
||||
<span id='name'>
|
||||
<label for="name">Name</label><br>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-field col s4">
|
||||
<span id='season'>
|
||||
<label for="season">Season</label><br>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-field col s4">
|
||||
<span id='episode'>
|
||||
<label for="episode">Episode</label><br>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col s12"></div>
|
||||
<div class="input-field col s12">
|
||||
<!-- <input placeholder="" id="video_files" type="text" class="validate">
|
||||
<label for="video_files">Video files</label> -->
|
||||
<span id='video_files'>
|
||||
<label>Video files</label><br>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-field col s12">
|
||||
<!-- <input placeholder="" id="subtitles" type="text" class="validate">
|
||||
<label for="subtitles">Subtitles</label> -->
|
||||
<span id='subtitles'>
|
||||
<label>Subtitles</label><br>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-field col s12">
|
||||
<!-- <input placeholder="" id="trash" type="text" class="validate">
|
||||
<label for="trash">Trash</label> -->
|
||||
<span id='trash'>
|
||||
<label>Trash</label><br>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn waves-effect waves-light" type="submit" name="action">Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script type="text/javascript" src="js/env_variables.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user