From 0172a7d0f790e61591a69268e2ab288d5f273b2b Mon Sep 17 00:00:00 2001 From: "impr.visible@gmail.com" Date: Tue, 10 Jan 2023 21:11:10 +0100 Subject: [PATCH] v5.5.0 Changelog: - Working on the new library types, "Others" - You can rescan all libraries from settings or the lib page - Filter added, you can select the movies genre, and order by alphabetic, rate, date in ascendant, or descendant way --- .gitignore | 3 +- Dockerfile | 10 + README.md | 25 +- app.py | 729 +++++++++++++++++++++++++++---------- config.ini | 2 +- intro.py | 239 +++++++++--- requirements.txt | 1 + static/css/style.css | 320 +++++++++++++++- static/js/allFilms.js | 284 ++++++++++++++- static/js/allSeries.js | 286 +++++++++++++-- static/js/channel.js | 10 +- static/js/login.js | 3 +- static/js/main.js | 42 ++- static/js/movie.js | 81 ++++- static/js/other.js | 166 +++++++++ static/js/otherVideo.js | 154 ++++++++ static/js/serie.js | 21 +- static/js/settings.js | 48 ++- static/js/tv.js | 37 +- static/lang/languages.json | 518 ++++++++++++++++++++++++-- templates/allFilms.html | 30 +- templates/allSeries.html | 27 +- templates/consoles.html | 6 +- templates/film.html | 23 +- templates/games.html | 2 +- templates/header.html | 5 +- templates/login.html | 2 +- templates/other.html | 29 ++ templates/otherVideo.html | 16 + templates/profil.html | 4 +- templates/serie.html | 16 +- templates/settings.html | 12 + templates/tv.html | 5 +- 33 files changed, 2775 insertions(+), 381 deletions(-) create mode 100644 Dockerfile create mode 100644 static/js/other.js create mode 100644 static/js/otherVideo.js create mode 100644 templates/other.html create mode 100644 templates/otherVideo.html diff --git a/.gitignore b/.gitignore index 9c80c21..977a3d4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ static/bios/* !static/bios/readme.txt .vscode/tasks.json static/img/* -.venv \ No newline at end of file +.venv +database.db-journal diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb1d74d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10-slim-buster + +RUN apt-get update && apt-get install -y ffmpeg && apt-get install -y git + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . /app + +CMD ["python", "/app/app.py"] \ No newline at end of file diff --git a/README.md b/README.md index 713b333..1d2c685 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ This is what you have to do to get started with Chocolate : * Go here: [https://www.myqnap.org/product/chocolate81/](https://www.myqnap.org/product/chocolate81/) * Enjoy ! +#### For Docker +* Clone the repository +* Execute `docker build -t chocolate .` +* When the build is finished, execute `docker run chocolate` +* Enjoy ! + ### Files organizations #### For Movies : @@ -167,6 +173,7 @@ Don't forget to give the project a star! Thanks again! - [ ] Add support to trakt - [ ] Use the GPU to encode videos if possible - [ ] Change season with the buttons +- [ ] Add logs ### Work in progress - [ ] Detect series intro and skip them @@ -196,6 +203,8 @@ Don't forget to give the project a star! Thanks again! - [X] Edit movie metadata directly on the website - [X] Add a download button - [X] Add TV with M3U playlist +- [X] Added new type of library, "other", for files that are not movies, series, games, tv +- [X] Add 2 buttons, one rescan the library, and the other one in the settings to rescan all the libraries ## Languages to translate @@ -212,16 +221,16 @@ Don't forget to give the project a star! Thanks again! - [X] BS: Bosnian - [X] BG: Bulgarian - [X] CA: Catalan -- [ ] NY: Chichewa +- [X] NY: Chichewa - [X] CO: Corsican - [X] HR: Croatian -- [ ] CS: Czech -- [ ] DA: Danish -- [ ] NL: Dutch +- [X] CS: Czech +- [X] DA: Danish +- [X] NL: Dutch - [X] EN: English -- [ ] EO: Esperanto -- [ ] ET: Estonian -- [ ] FI: Finnish +- [X] EO: Esperanto +- [X] ET: Estonian +- [X] FI: Finnish - [X] FR: French - [ ] FY: Frisian - [ ] GL: Galician @@ -256,7 +265,7 @@ Don't forget to give the project a star! Thanks again! - [ ] MS: Malay - [ ] ML: Malayalam - [ ] MT: Maltese -- [ ] ZH: Mandarin +- [X] ZH: Mandarin - [ ] MI: Maori - [ ] MR: Marathi - [ ] MN: Mongolian diff --git a/app.py b/app.py index dc52b2d..05a4c64 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,7 @@ from time import mktime from PIL import Image from pypresence import Presence -import requests, os, subprocess, configparser, socket, datetime, subprocess, socket, platform, GPUtil, json, time, sqlalchemy, warnings, re, zipfile, ast, git, pycountry +import requests, os, subprocess, configparser, socket, datetime, subprocess, socket, platform, GPUtil, json, time, sqlalchemy, warnings, re, zipfile, ast, git, pycountry, zlib start_time = mktime(time.localtime()) @@ -46,7 +46,10 @@ class Users(db.Model, UserMixin): def __init__(self, name, password, profilePicture, accountType): self.name = name - self.password = generate_password_hash(password) + if password != None: + self.password = generate_password_hash(password) + else: + self.password = None self.profilePicture = profilePicture self.accountType = accountType @@ -54,6 +57,8 @@ def __repr__(self) -> str: return f'' def verify_password(self, pwd): + if self.password == None: + return True return check_password_hash(self.password, pwd) class Movies(db.Model): @@ -220,20 +225,28 @@ def __repr__(self) -> str: return f"" -class TVChannels(db.Model): - id = db.Column(db.Integer, primary_key=True) - libraryName = db.Column(db.String(255), primary_key=True) - name = db.Column(db.String(255), primary_key=True) - path = db.Column(db.String(255)) +class OthersVideos(db.Model): + videoHash = db.Column(db.String(255), primary_key=True) + title = db.Column(db.String(255), primary_key=True) + slug = db.Column(db.String(255)) + mediaType = db.Column(db.String(255)) + banner = db.Column(db.String(255)) + duration = db.Column(db.String(255)) + libraryName = db.Column(db.String(255)) + vues = db.Column(db.Text, default=str({})) - def __init__(self, id, libraryName, name, path): - self.id = id + def __init__(self, videoHash, title, slug, mediaType, banner, duration, libraryName, vues): + self.videoHash = videoHash + self.title = title + self.slug = slug + self.mediaType = mediaType + self.banner = banner + self.duration = duration self.libraryName = libraryName - self.name = name - self.path = path + self.vues = vues def __repr__(self) -> str: - return f"" + return f"" class Language(db.Model): language = db.Column(db.String(255), primary_key=True) @@ -317,6 +330,7 @@ def load_user(id): if apiKeyTMDB == "Empty": print("Follow this tutorial to get your TMDB API Key : https://developers.themoviedb.org/3/getting-started/introduction") tmdb.api_key = config["APIKeys"]["TMDB"] +tmdb.language = config["ChocolateSettings"]["language"] def searchGame(game, console): url = f"https://www.igdb.com/search_autocomplete_all?q={game.replace(' ', '%20')}" @@ -349,9 +363,7 @@ def IGDBRequest(url, console): token = token["access_token"] headers = { - "Accept": "application/json", - "Authorization": f"Bearer {token}", - "Client-ID": clientID + "Accept": "application/json", "Authorization": f"Bearer {token}", "Client-ID": clientID } games = response.json()["game_suggest"] @@ -370,16 +382,7 @@ def IGDBRequest(url, console): platforms = [i["abbreviation"] for i in gamePlatforms] realConsoleName = { - "GB": "Game Boy", - "GBA": "Game Boy Advance", - "GBC": "Game Boy Color", - "N64": "Nintendo 64", - "NES": "Nintendo Entertainment System", - "NDS": "Nintendo DS", - "SNES": "Super Nintendo Entertainment System", - "Sega Master System": "Sega Master System", - "Sega Mega Drive": "Sega Mega Drive", - "PS1": "PS1" + "GB": "Game Boy", "GBA": "Game Boy Advance", "GBC": "Game Boy Color", "N64": "Nintendo 64", "NES": "Nintendo Entertainment System", "NDS": "Nintendo DS", "SNES": "Super Nintendo Entertainment System", "Sega Master System": "Sega Master System", "Sega Mega Drive": "Sega Mega Drive", "PS1": "PS1" } if realConsoleName[console] not in platforms and console not in platforms: @@ -405,13 +408,7 @@ def IGDBRequest(url, console): genres = ", ".join(genres) gameData = { - "title": game["name"], - "cover": game["cover"]["url"].replace("//", "https://"), - "description": game["summary"], - "note": game["total_rating"], - "date": game["first_release_date"], - "genre": genres, - "id": game["id"] + "title": game["name"], "cover": game["cover"]["url"].replace("//", "https://"), "description": game["summary"], "note": game["total_rating"], "date": game["first_release_date"], "genre": genres, "id": game["id"] } return gameData except: @@ -519,7 +516,7 @@ def translate(string): def getMovies(libraryName): - + allMoviesNotSorted = [] movie = Movie() path = Libraries.query.filter_by(libName=libraryName).first().libFolder try: @@ -557,7 +554,11 @@ def getMovies(libraryName): if not exists: movie = Movie() try: - search = movie.search(movieTitle, adult=True) + match = re.search(r'\((\d{4})\)$', movieTitle) + if match: + search = movie.search(movieTitle, year=match.group(1), adult=True) + else: + search = movie.search(movieTitle, adult=True) except TMDbException: print(TMDbException) allMoviesNotSorted.append(search) @@ -591,6 +592,7 @@ def getMovies(libraryName): banniere = f"https://image.tmdb.org/t/p/original{res.backdrop_path}" realTitle, extension = os.path.splitext(originalMovieTitle) rewritedName = realTitle.replace(" ", "_") + with open(f"{dirPath}/static/img/mediaImages/{rewritedName}_Cover.png", "wb") as f: f.write(requests.get(movieCoverPath).content) try: @@ -601,7 +603,7 @@ def getMovies(libraryName): except: os.rename(f"{dirPath}/static/img/mediaImages/{rewritedName}_Cover.png", f"{dirPath}/static/img/mediaImages/{rewritedName}_Cover.webp") movieCoverPath = "/static/img/broken.webp" - + with open(f"{dirPath}/static/img/mediaImages/{rewritedName}_Banner.png", "wb") as f: f.write(requests.get(banniere).content) if res.backdrop_path == None: @@ -617,7 +619,6 @@ def getMovies(libraryName): os.remove(f"{dirPath}/static/img/mediaImages/{rewritedName}_Banner.png") banniere = f"/static/img/mediaImages/{rewritedName}_Banner.webp" except: - os.rename(f"{dirPath}/static/img/mediaImages/{rewritedName}_Banner.png", f"{dirPath}/static/img/mediaImages/{rewritedName}_Banner.webp") banniere = "/static/img/brokenBanner.webp" description = res.overview @@ -763,6 +764,56 @@ def getMovies(libraryName): db.session.delete(movie) db.session.commit() +class EpisodeGroup(): + def __init__(self, **entries): + if "success" in entries and entries["success"] is False: + raise TMDbException(entries["status_message"]) + for key, value in entries.items(): + if isinstance(value, list): + value = [EpisodeGroup(**item) if isinstance(item, dict) else item for item in value] + elif isinstance(value, dict): + value = EpisodeGroup(**value) + setattr(self, key, value) + +def getEpisodeGroupe(apikey, serieId, language="EN"): + details = show.details(serieId) + seasonsInfo = details.seasons + serieTitle = details.name + url = f"https://api.themoviedb.org/3/tv/{serieId}/episode_groups?api_key={apikey}&language={language}" + r = requests.get(url) + data = r.json() + if data["results"]: + print(f"Found {len(data['results'])} episode groups for {serieTitle}") + for episodeGroup in data["results"]: + index = data["results"].index(episodeGroup) + 1 + name = episodeGroup["name"] + episodeCount = episodeGroup["episode_count"] + description = episodeGroup["description"] + print(f"{index}: Found {episodeCount} episodes for {name} ({description})") + print("0: Use the default episode group") + selectedEpisodeGroup = int(input("Which episode group do you want to use ? ")) + if selectedEpisodeGroup > 0: + theEpisodeGroup = data["results"] + episode_group_data_url = f"https://api.themoviedb.org/3/tv/episode_group/{theEpisodeGroup['id']}?api_key={apikey}&language={language}" + r = requests.get(episode_group_data_url) + data = r.json() + + seasons = data["groups"] + for season in seasons: + seasonId = season["id"] + seasonName = season["name"] + episodesNumber = len(season["episodes"]) + releaseDate = season["episodes"][0]["air_date"] + seasonNumber = season["order"] + seasonDescription = season.overview + seasonPoster = "/static/img/broken.png" + + #get the season from seasonId + seasonsInfo = [season for season in seasonsInfo if season.id == seasonId] + if seasonsInfo: + seasonsInfo = EpisodeGroup(id=seasonId, name=seasonName, episode_count=episodesNumber, air_date=releaseDate, season_number=seasonNumber, overview=seasonDescription, poster_path=seasonPoster) + else: + seasonsInfo = seasonsInfo def getSeries(libraryName): allSeriesPath = Libraries.query.filter_by(libName=libraryName).first().libFolder @@ -882,10 +933,12 @@ def getSeries(libraryName): res = bestMatch serieId = res.id - + if (db.session.query(Series).filter_by(originalName=serieTitle).first() is not None): + serieId = db.session.query(Series).filter_by(originalName=serieTitle).first().id exists = db.session.query(Series).filter_by(id=serieId).first() is not None details = show.details(serieId) seasonsInfo = details.seasons + rewritedName = serieTitle.replace(" ", "_") seasonsNumber = [] @@ -894,10 +947,14 @@ def getSeries(libraryName): if os.path.isdir(f"{allSeriesPath}/{originalSerieTitle}/{season}"): season = re.sub(r"\D", "", season) seasonsNumber.append(int(season)) - - if not exists and seasonsNumber: + + seasons = Seasons.query.filter_by(serie=serieId).all() + seasonInDb = [season for season in seasons] + + if (not exists and seasonsNumber) and not (len(seasonsNumber) != len(seasonInDb)): details = show.details(serieId) seasonsInfo = details.seasons + name = res.name serieCoverPath = f"https://image.tmdb.org/t/p/original{res.poster_path}" banniere = f"https://image.tmdb.org/t/p/original{res.backdrop_path}" @@ -921,7 +978,6 @@ def getSeries(libraryName): description = res["overview"] note = res.vote_average date = res.first_air_date - serieId = res.id cast = details.credits.cast runTime = details.episode_run_time duration = "" @@ -999,6 +1055,8 @@ def getSeries(libraryName): seasonNumber = season.season_number seasonId = season.id seasonName = season.name + seasonDescription = season.overview + seasonPoster = season.poster_path try: exists = Seasons.query.filter_by(seasonId=seasonId).first() is not None @@ -1010,8 +1068,7 @@ def getSeries(libraryName): numberOfEpisodesInDatabase = Episodes.query.filter_by(seasonId=seasonId).count() sameNumberOfEpisodes = numberOfEpisodes == numberOfEpisodesInDatabase if seasonNumber in seasonsNumber and sameNumberOfEpisodes == False and numberOfEpisodes > 0: - seasonDescription = season.overview - seasonCoverPath = (f"https://image.tmdb.org/t/p/original{season.poster_path}") + seasonCoverPath = (f"https://image.tmdb.org/t/p/original{seasonPoster}") if not os.path.exists(f"{dirPath}/static/img/mediaImages/{rewritedName}S{seasonNumber}_Cover.png"): with open(f"{dirPath}/static/img/mediaImages/{rewritedName}S{seasonNumber}_Cover.png", "wb") as f: f.write(requests.get(seasonCoverPath).content) @@ -1285,20 +1342,59 @@ def getGames(libraryName): db.session.delete(game) db.session.commit() - +def getOthersVideos(library): + allVideosPath = Libraries.query.filter_by(libName=library).first().libFolder + allVideos = os.listdir(allVideosPath) + supportedVideoTypes = [".mp4", ".webm", ".mkv"] + + allVideos = [ video for video in allVideos if os.path.splitext(video)[1] in supportedVideoTypes ] + + mediaTypes = { + ".mp4": "video/mp4", + ".webm": "video/webm", + ".mkv": "video/x-matroska", + } + for video in allVideos: + title, extension = os.path.splitext(video) + + index = allVideos.index(video) + 1 + percentage = index * 100 / len(allVideos) + + loadingFirstPart = ("•" * int(percentage * 0.2))[:-1] + loadingFirstPart = f"{loadingFirstPart}➤" + loadingSecondPart = "•" * (20 - int(percentage * 0.2)) + loading = f"{str(int(percentage)).rjust(3)}% | [\33[32m{loadingFirstPart} \33[31m{loadingSecondPart}\33[0m] | {title} | {index}/{len(allVideos)} " + print("\033[?25l", end="") + print(loading, end="\r", flush=True) + + slug = f"{allVideosPath}/{video}" + + with open(slug, "rb") as f: + video_hash = zlib.crc32(f.read()) + + # Conversion du hash en chaîne hexadécimale + video_hash_hex = hex(video_hash)[2:] + + # Récupération des 10 premiers caractères + videoHash = video_hash_hex[:10] + exists = OthersVideos.query.filter_by(videoHash=videoHash).first() is not None + + if extension in supportedVideoTypes and not exists: + videoType = mediaTypes[extension] + videoDuration = length_video(slug) + middle = videoDuration // 2 + banner = f"/static/img/mediaImages/Other_Banner_{title.replace(' ', '_')}.png" + if os.path.exists(f"{dir}{banner}") == False: + subprocess.run(["ffmpeg", "-i", slug, "-vf", f"select='eq(n,{middle})'", "-vframes", "1", f"{dir}{banner}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + video = OthersVideos(videoHash=videoHash, title=title, slug=slug, mediaType=videoType, banner=banner, duration=videoDuration, libraryName=library, vues=str({})) + db.session.add(video) + db.session.commit() def length_video(path: str) -> float: seconds = subprocess.run( [ - "ffprobe", - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - path, + "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path, ], stdout=subprocess.PIPE, text=True, @@ -1350,15 +1446,14 @@ def before_request(): users = Users.query.all() for library in libraries: del library["_sa_instance_state"] - - #get username with Flask- if current_user.is_authenticated: username = current_user.name for library in libraries: - if library["availableFor"] != None: - availableFor = library["availableFor"].split(",") - if username not in availableFor: - libraries.remove(library) + for library2 in libraries: + if library2["availableFor"] != None: + availableFor = library2["availableFor"].split(",") + if username not in availableFor: + libraries.remove(library2) libraries = sorted(libraries, key=lambda k: k['libName']) libraries = sorted(libraries, key=lambda k: k['libType']) @@ -1367,6 +1462,7 @@ def before_request(): g.libraries = libraries g.users = users g.languageCode = language + g.currentUser = current_user @app.route('/offline') @@ -1451,6 +1547,81 @@ def create_m3u8_quality(quality, movieID): return response +@app.route("/other-video/", methods=["GET"]) +def create_other_m3u8(hash): + other = OthersVideos.query.filter_by(videoHash=hash).first() + slug = other.slug + library = other.libraryName + theLibrary = Libraries.query.filter_by(libName=library).first() + path = theLibrary.libFolder + video_path = slug + duration = length_video(video_path) + file = f""" +#EXTM3U + +#EXT-X-VERSION:4 +#EXT-X-TARGETDURATION:{CHUNK_LENGTH} +#EXT-X-MEDIA-SEQUENCE:1 + """ + + for i in range(0, int(duration), CHUNK_LENGTH): + file += f""" +#EXTINF:{float(CHUNK_LENGTH)}, +/chunkOther/{hash}-{(i // CHUNK_LENGTH) + 1}.ts + """ + + file += """ +#EXT-X-ENDLIST""" + + response = make_response(file) + response.headers.set("Content-Type", "application/x-mpegURL") + response.headers.set("Range", "bytes=0-4095") + response.headers.set("Accept-Encoding", "*") + response.headers.set("Access-Control-Allow-Origin", f"http://{local_ip}:{serverPort}") + response.headers.set( + "Content-Disposition", "attachment", filename=f"{hash}.m3u8" + ) + + return response + +@app.route("/other-video//", methods=["GET"]) +def create_other_m3u8_quality(quality, hash): + other = OthersVideos.query.filter_by(videoHash=hash).first() + slug = other.slug + library = other.libraryName + theLibrary = Libraries.query.filter_by(libName=library).first() + path = theLibrary.libFolder + video_path = slug + duration = length_video(video_path) + file = f""" +#EXTM3U + +#EXT-X-VERSION:4 +#EXT-X-TARGETDURATION:{CHUNK_LENGTH} +#EXT-X-MEDIA-SEQUENCE:1 + """ + + for i in range(0, int(duration), CHUNK_LENGTH): + file += f""" +#EXTINF:{float(CHUNK_LENGTH)}, +/chunkOther/{quality}/{hash}-{(i // CHUNK_LENGTH) + 1}.ts + """ + + file += """ +#EXT-X-ENDLIST""" + + response = make_response(file) + response.headers.set("Content-Type", "application/x-mpegURL") + response.headers.set("Range", "bytes=0-4095") + response.headers.set("Accept-Encoding", "*") + response.headers.set("Access-Control-Allow-Origin", f"http://{local_ip}:{serverPort}") + response.headers.set( + "Content-Disposition", "attachment", filename=f"{hash}.m3u8" + ) + + return response + + @app.route("/videoSerie/", methods=["GET"]) def create_serie_m3u8(episodeId): episode = Episodes.query.filter_by(episodeId=episodeId).first() @@ -1786,6 +1957,122 @@ def get_chunk_quality(quality, movieID, idx=0): return response +@app.route("/chunkOther/-.ts", methods=["GET"]) +def get_chunk_other(hash, idx=0): + seconds = (idx - 1) * CHUNK_LENGTH + movie = OthersVideos.query.filter_by(videoHash=hash).first() + video_path = movie.slug + + time_start = str(datetime.timedelta(seconds=seconds)) + time_end = str(datetime.timedelta(seconds=seconds + CHUNK_LENGTH)) + logLevelValue = "error" + + + command = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + logLevelValue, + "-ss", + time_start, + "-to", + time_end, + "-i", + video_path, + "-output_ts_offset", + time_start, + "-c:v", + "libx264", + "-c:a", + "aac", + "-b:a", + "196k", + "-ac", + "2", + "-f", + "mpegts", + "pipe:1", + ] + pipe = subprocess.Popen(command, stdout=subprocess.PIPE) + + response = make_response(pipe.stdout.read()) + response.headers.set("Content-Type", "video/MP2T") + response.headers.set("Range", "bytes=0-4095") + response.headers.set("Accept-Encoding", "*") + response.headers.set("Access-Control-Allow-Origin", f"http://{local_ip}:{serverPort}") + response.headers.set( + "Content-Disposition", "attachment", filename=f"{hash}-{idx}.ts" + ) + + return response + +@app.route("/chunkOther//-.ts", methods=["GET"]) +def get_chunk_other_quality(quality, hash, idx=0): + seconds = (idx - 1) * CHUNK_LENGTH + movie = OthersVideos.query.filter_by(videoHash=hash).first() + video_path = movie.slug + + time_start = str(datetime.timedelta(seconds=seconds)) + time_end = str(datetime.timedelta(seconds=seconds + CHUNK_LENGTH)) + videoProperties = get_video_properties(video_path) + width = videoProperties["width"] + height = videoProperties["height"] + newWidth = int(float(quality)) + newHeight = round(float(width) / float(height) * newWidth) + if (newHeight % 2) != 0: + newHeight += 1 + + bitrate = { + "1080": "192k", + "720": "192k", + "480": "128k", + "360": "128k", + "240": "96k", + "144": "64k", + } + + logLevelValue = "error" + command = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + logLevelValue, + "-ss", + time_start, + "-to", + time_end, + "-i", + video_path, + "-output_ts_offset", + time_start, + "-c:v", + "libx264", + "-vf", + f"scale={newHeight}:{newWidth}", + "-c:a", + "aac", + "-b:a", + bitrate[quality], + "-ac", + "2", + "-f", + "mpegts", + "pipe:1", + ] + + pipe = subprocess.Popen(command, stdout=subprocess.PIPE) + + response = make_response(pipe.stdout.read()) + response.headers.set("Content-Type", "video/MP2T") + response.headers.set("Range", "bytes=0-4095") + response.headers.set("Accept-Encoding", "*") + response.headers.set("Access-Control-Allow-Origin", f"http://{local_ip}:{serverPort}") + response.headers.set( + "Content-Disposition", "attachment", filename=f"{hash}-{idx}.ts" + ) + + return response + @app.route("/chunkCaption///.vtt", methods=["GET"]) def chunkCaption(movieID, subtitleType, index): subtitleType = subtitleType.lower() @@ -1797,17 +2084,7 @@ def chunkCaption(movieID, subtitleType, index): video_path = f"{path}/{slug}" if subtitleType == "srt": extractCaptionsCommand = [ - "ffmpeg", - "-hide_banner", - "-loglevel", - "error", - "-i", - video_path, - "-map", - f"0:{index}", - "-f", - "webvtt", - "pipe:1", + "ffmpeg", "-hide_banner", "-loglevel", "error", "-i", video_path, "-map", f"0:{index}", "-f", "webvtt", "pipe:1", ] extractCaptions = subprocess.run(extractCaptionsCommand, stdout=subprocess.PIPE) @@ -1830,6 +2107,7 @@ def settings(): if request.method == "POST": accountName = request.form["name"] accountPassword = request.form["password"] + print(request.form) try: f = request.files['profilePicture'] name, extension = os.path.splitext(f.filename) @@ -1841,9 +2119,10 @@ def settings(): except: profilePicture = "/static/img/defaultUserProfilePic.png" accountTypeInput = request.form["type"] - - if accountTypeInput == "Kid": - accountPassword = "" + print(accountPassword) + print(type(accountPassword)) + if accountTypeInput == "Kid" or accountPassword == "": + accountPassword = None new_user = Users(name=accountName, password=accountPassword, profilePicture=profilePicture, accountType=accountTypeInput) db.session.add(new_user) @@ -1881,7 +2160,7 @@ def login(): allUsersDict = [] theresAnAdmin = False for user in allUsers: - userDict = {"name": user.name, "profilePicture": user.profilePicture, "accountType": user.accountType} + userDict = {"name": user.name, "profilePicture": user.profilePicture, "accountType": user.accountType, "passwordEmpty": "yes" if user.password == None else "no"} if user.accountType == "Admin": theresAnAdmin = True allUsersDict.append(userDict) @@ -1948,6 +2227,7 @@ def logout(): return redirect(url_for("login")) @app.route("/profil", methods=["GET", "POST"]) +@login_required def profil(): user = current_user currentUsername = user.name @@ -1969,7 +2249,10 @@ def profil(): logout_user() db.session.commit() if userToEdit.password != generate_password_hash(password) and len(password)>0: - userToEdit.password = generate_password_hash(password) + if password == "": + userToEdit.password = None + else: + userToEdit.password = generate_password_hash(password) db.session.commit() if userToEdit.profilePicture != profilePicture and profilePicture != "/static/img/defaultUserProfilePic.png": f = request.files['profilePicture'] @@ -2090,7 +2373,8 @@ def createLib(): "movies": "film", "series": "videocam", "games": "game-controller", - "tv": "tv" + "tv": "tv", + "other": "desktop" } libPath = libPath.replace("/", "\\") @@ -2155,7 +2439,7 @@ def getSimilarSeries(seriesId) -> list: @app.route("/getMovieData/", methods=["GET", "POST"]) def getMovieData(movieId): - exists = db.session.query(Movies).filter_by(id=movieId).first() is not None + exists = Movies.query.filter_by(id=movieId).first() is not None if exists: movie = Movies.query.filter_by(id=movieId).first().__dict__ del movie["_sa_instance_state"] @@ -2164,10 +2448,19 @@ def getMovieData(movieId): else: return json.dumps("Not Found", ensure_ascii=False) +@app.route("/getOtherData/", methods=["GET", "POST"]) +def getOtherData(videoHash): + exists = OthersVideos.query.filter_by(videoHash=videoHash).first() is not None + if exists: + other = OthersVideos.query.filter_by(videoHash=videoHash).first().__dict__ + del other["_sa_instance_state"] + return json.dumps(other, ensure_ascii=False) + else: + return json.dumps("Not Found", ensure_ascii=False) @app.route("/getSerieData/", methods=["GET", "POST"]) def getSeriesData(serieId): - exists = db.session.query(Series).filter_by(id=serieId).first() is not None + exists = Series.query.filter_by(id=serieId).first() is not None if exists: serie = Series.query.filter_by(id=serieId).first().__dict__ serie["seasons"] = getSerieSeasons(serie["id"]) @@ -2290,8 +2583,6 @@ def editMovie(title, library): theMovie.genre = movieGenre - - casts = movieInfo.casts.cast[:5] theCast = [] for cast in casts: @@ -2382,7 +2673,7 @@ def editMovie(title, library): theMovie.banner = banniere db.session.commit() - return redirect(url_for("movies", library=theMovie.libraryName)) + return redirect(url_for("moviesLib", library=theMovie.libraryName)) @app.route("/season/") @login_required @@ -2426,33 +2717,39 @@ def getEpisodeData(serieName, seasonId, episodeId): response = {"response": "Not Found"} return json.dumps(response, ensure_ascii=False, default=str) -@app.route("/categories") -def categories(): - #return a list of all categories - categoriesList = [] - movies = Movies.query.all() - moviesDict = [ movie.__dict__ for movie in movies ] - for movie in moviesDict: - movieGenre = movie["genre"] - movieGenre = json.loads(movieGenre) - for genre in movieGenre: - if genre not in categoriesList: - categoriesList.append(genre) - categoriesList.sort() - return json.dumps(categoriesList, ensure_ascii=False, default=str) - -@app.route("/category/") -def category(category): - movies = Movies.query.all() - movieList = [] - moviesDict = [ movie.__dict__ for movie in movies ] - for movie in moviesDict: - movieGenre = movie["genre"] - movieGenre = json.loads(movieGenre) - if category in movieGenre: - del movie["_sa_instance_state"] - movieList.append(movie) - return json.dumps(movieList, ensure_ascii=False, default=str) +@app.route("/rescan/", methods=["GET", "POST"]) +@login_required +def rescan(library): + exists = Libraries.query.filter_by(libName=library).first() is not None + if exists: + library = Libraries.query.filter_by(libName=library).first().__dict__ + if library["libType"] == "series": + getSeries(library["libName"]) + elif library["libType"] == "movies": + getMovies(library["libName"]) + elif library["libType"] == "games": + getGames(library["libName"]) + elif library["libType"] == "other": + getOthersVideos(library["libName"]) + return json.dumps(True) + return json.dumps(False) + +@app.route("/rescanAll") +@login_required +def rescanAll(): + library = Libraries.query.all() + for lib in library: + lib = lib.__dict__ + if lib["libType"] == "series": + getSeries(lib["libName"]) + elif lib["libType"] == "movies": + getMovies(lib["libName"]) + elif lib["libType"] == "games": + getGames(lib["libName"]) + elif lib["libType"] == "other": + getOthersVideos(lib["libName"]) + return json.dumps(True) + @app.route("/movies/") @login_required @@ -2470,12 +2767,7 @@ def moviesLib(library): searchedFilmsUp0 = len(moviesDict) == 0 errorMessage = "Verify that the path is correct, or restart Chocolate" routeToUse = f"/getAllMovies/{thisLibrary.libName}" - return render_template("allFilms.html", - conditionIfOne=searchedFilmsUp0, - errorMessage=errorMessage, - routeToUse=routeToUse, - canDownload=canDownload - ) + return render_template("allFilms.html", conditionIfOne=searchedFilmsUp0, errorMessage=errorMessage, routeToUse=routeToUse, canDownload=canDownload, library=library, rescanLink=f"/rescan/{library}") else: return redirect(url_for("home")) @@ -2488,13 +2780,36 @@ def seriesLibrary(library): searchedSeriesUp0 = len(seriesDict.keys()) == 0 errorMessage = "Verify that the path is correct, or restart Chocolate" routeToUse = f"/getAllSeries/{library}" - return render_template("allSeries.html", conditionIfOne=searchedSeriesUp0, errorMessage=errorMessage, routeToUse=routeToUse) + return render_template("allSeries.html", conditionIfOne=searchedSeriesUp0, errorMessage=errorMessage, routeToUse=routeToUse, rescanLink=f"/rescan/{library}", library=library) @app.route("/tv/") @login_required def tvLibrary(library): return render_template("tv.html") +@app.route("/other/") +@login_required +def otherLibrary(library): + routeToUse = f"/getAllOther/{library}" + config = configparser.ConfigParser() + config.read(os.path.join(dir, 'config.ini')) + canDownload = config["ChocolateSettings"]["allowDownload"].lower() == "true" + return render_template("other.html", library=library, routeToUse=routeToUse, canDownload=canDownload, rescanLink=f"/rescan/{library}") + +@app.route("/downloadOther/") +@login_required +def downloadOther(videoHash): + video = OthersVideos.query.filter_by(videoHash=videoHash).first() + video = video.__dict__ + del video["_sa_instance_state"] + return send_file(video["slug"], as_attachment=True) + +@app.route("/getAllOther/") +def getAllOther(library): + other = OthersVideos.query.filter_by(libraryName=library).all() + otherDict = [ video.__dict__ for video in other ] + return json.dumps(otherDict, ensure_ascii=False, default=str) + @app.route("/tv//") @login_required def tvChannel(library, id): @@ -2508,11 +2823,18 @@ def tvChannel(library, id): with open(libFolder, "r", encoding="utf-8") as f: m3u = f.readlines() m3u.pop(0) - while m3u[0] == "\n": - m3u.pop(0) - channelURL = m3u[int(id)+1] - - return render_template("channel.html", channelURL=channelURL) + for ligne in m3u: + if not ligne.startswith(("#EXTINF", "http")): + m3u.remove(ligne) + line = m3u[int(id)] + nextLine = m3u[int(id)+1] + if line.startswith("#EXTINF"): + line = nextLine + try: + channelName = line.split(",")[1].replace("\n", "") + except IndexError: + channelName = "Channel" + return render_template("channel.html", channelURL=line, channelName=channelName) @app.route("/getChannels/") def getChannels(library): @@ -2533,7 +2855,7 @@ def getChannels(library): #get the channels by getting 2 lines at a time channels = [] for i in m3u: - if i.startswith(("#EXTVLCOPT", "#EXTGRP")): + if not i.startswith(("#EXTINF", "http")): m3u.remove(i) elif i == "\n": m3u.remove(i) @@ -2560,11 +2882,12 @@ def getChannels(library): tvg_logo = match.group(1) data["logo"] = tvg_logo else: - brokenPath = "/static/img/borkenBanner.webp" + brokenPath = "/static/img/brokenBanner.webp" data["logo"] = brokenPath #print(data["logo"]) channels.append(data) - print(f"Channel {((i+1)//2)+1} added {((i+1)//2)+1}/{int(len(m3u)/2)}") + #order the channels by name + channels = sorted(channels, key=lambda k: k['name']) return json.dumps(channels) def is_valid_url(url): @@ -2577,7 +2900,7 @@ def is_valid_url(url): @app.route("/games/") @login_required def games(library): - return render_template("consoles.html") + return render_template("consoles.html", rescanLink=f"/rescan/{library}", library=library) @app.route("/getAllConsoles/") def getAllConsoles(library): @@ -2626,11 +2949,7 @@ def console(library, consoleName): errorMessage = "Verify that the games path is correct" routeToUse = "/getAllGames" - return render_template("games.html", - conditionIfOne=searchedGamesUp0, - errorMessage=errorMessage, - routeToUse=routeToUse, - consoleName=consoleName + return render_template("games.html", conditionIfOne=searchedGamesUp0, errorMessage=errorMessage, routeToUse=routeToUse, consoleName=consoleName ) else: return json.dumps({"response": "Not Found"}, ensure_ascii=False, default=str) @@ -2662,17 +2981,7 @@ def game(console, gameSlug): bios = f"/bios/{consoleName}" del game["_sa_instance_state"] scripts = { - "Gameboy": f'', - "Gameboy Advance": f'', - "Gameboy Color": f'', - "Nintendo 64": f'', - "Nintendo Entertainment System": f'', - "Nintendo DS": f'', - "Super Nintendo Entertainment System": f'', - "Sega Mega Drive": f'', - "Sega Master System": f'', - "Sega Saturn": f'', - "PS1": f'', + "Gameboy": f'', "Gameboy Advance": f'', "Gameboy Color": f'', "Nintendo 64": f'', "Nintendo Entertainment System": f'', "Nintendo DS": f'', "Super Nintendo Entertainment System": f'', "Sega Mega Drive": f'', "Sega Master System": f'', "Sega Saturn": f'', "PS1": f'', } theScript = scripts[consoleName] theScript = Markup(theScript) @@ -2722,8 +3031,7 @@ def searchInAllMovies(library, search): def searchInAllSeries(library, search): search = search.replace("%20", " ") series = Series.query.filter_by(libraryName=library).all() - #SELECT * FROM Series WHERE ((name, originalName,description,cast,date) like %search%) - series = [m for m in series if any(search in x.lower() for x in (m.name, m.originalName, m.description, m.cast, m.date, m.genre))] + series = [s for s in series if any(search.lower() in x.lower() for x in (s.name.lower(), s.originalName.lower(), s.description.lower(), s.cast.lower(), s.date.lower(), s.genre.lower()))] series = [i.__dict__ for i in series] for serie in series: del serie["_sa_instance_state"] @@ -2743,24 +3051,19 @@ def search(library, search): library = library.replace("%20", " ").replace("#", "") theLibrary = Libraries.query.filter_by(libName=library).first() theLibrary = theLibrary.__dict__ + config = configparser.ConfigParser() + config.read(os.path.join(dir, 'config.ini')) + canDownload = config["ChocolateSettings"]["allowDownload"].lower() == "true" if theLibrary["libType"] == "movies": searchedFilmsUp0 = False errorMessage = "Verify your search terms" routeToUse = f"/searchInMovies/{library}/{search}" - return render_template("allFilms.html", - conditionIfOne=searchedFilmsUp0, - errorMessage=errorMessage, - routeToUse=routeToUse, - ) + return render_template("allFilms.html", conditionIfOne=searchedFilmsUp0, errorMessage=errorMessage, routeToUse=routeToUse, canDownload=canDownload, library=library) elif theLibrary["libType"] == "series": searchedFilmsUp0 = False errorMessage = "Verify your search terms" routeToUse = f"/searchInSeries/{library}/{search}" - return render_template("allSeries.html", - conditionIfOne=searchedFilmsUp0, - errorMessage=errorMessage, - routeToUse=routeToUse, - ) + return render_template("allSeries.html", conditionIfOne=searchedFilmsUp0, errorMessage=errorMessage, routeToUse=routeToUse, library=library) @app.route("/movie/") @@ -2774,10 +3077,21 @@ def movie(movieID): link = f"/mainMovie/{movieID}".replace(" ", "%20") allCaptions = generateCaptionMovie(movieID) title = rewriteSlug - return render_template( - "film.html", slug=slug, allCaptions=allCaptions, title=title, movieUrl=link) + return render_template("film.html", slug=slug, allCaptions=allCaptions, title=title, movieUrl=link) return "Shut up and take my money !" + +@app.route("/otherVideo/") +@login_required +def otherVideo(videoHash): + if not videoHash.endswith("ttf"): + video = OthersVideos.query.filter_by(videoHash=videoHash).first() + video = video.__dict__ + link = f"/mainOther/{videoHash}".replace(" ", "%20") + return render_template("otherVideo.html", title=video["title"], movieUrl=link) + return "Shut up and take my money !" + + @app.route("/setVuesTimeCode/", methods=["POST"]) @login_required def setVuesTimeCode(): @@ -2797,6 +3111,25 @@ def setVuesTimeCode(): db.session.commit() return "ok" +@app.route("/setVuesOtherTimeCode/", methods=["POST"]) +@login_required +def setVuesOtherTimeCode(): + data = request.get_json() + videoHash = data["movieHASH"] + timeCode = data["timeCode"] + username = current_user.name + video = OthersVideos.query.filter_by(videoHash=videoHash).first() + actualVues = video.vues + + actualVues = ast.literal_eval(actualVues) + + actualVues[username] = timeCode + + actualVues = str(actualVues) + video.vues = actualVues + db.session.commit() + return "ok" + @app.route("/whoami", methods=["GET"]) @login_required def whoami(): @@ -2869,7 +3202,6 @@ def mainMovie(movieID): height = int(videoProperties["height"]) width = int(videoProperties["width"]) m3u8File = f"""#EXTM3U\n\n""" - #m3u8File += f"\n\n{generateAudioMovie(movieID)}" qualities = [144, 240, 360, 480, 720, 1080] file = [] for quality in qualities: @@ -2878,11 +3210,10 @@ def mainMovie(movieID): newHeight = int(float(width) / float(height) * newWidth) if (newHeight % 2) != 0: newHeight += 1 - m3u8Line = f"""#EXT-X-STREAM-INF:BANDWIDTH={newWidth*newWidth*1000},RESOLUTION={newHeight}x{newWidth}\n/video/{quality}/{movieID}\n""" + m3u8Line = f"""#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH={newWidth*newWidth*1000},RESOLUTION={newHeight}x{newWidth}\n/video/{quality}/{movieID}\n""" file.append(m3u8Line) - lastLine = f"#EXT-X-STREAM-INF:BANDWIDTH={width*height*1000},RESOLUTION={width}x{height}\n/video/{movieID}\n" + lastLine = f"""#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH={width*height*1000},RESOLUTION={width}x{height}\n/video/{movieID}\n""" file.append(lastLine) - file = file[::-1] file = "".join(file) m3u8File += file response = make_response(m3u8File) @@ -2939,6 +3270,41 @@ def mainSerie(episodeID): ) return response + +@app.route("/mainOther/") +def mainOther(otherHash): + movie = OthersVideos.query.filter_by(videoHash=otherHash).first() + video_path = movie.slug + videoProperties = get_video_properties(video_path) + height = int(videoProperties["height"]) + width = int(videoProperties["width"]) + m3u8File = f"""#EXTM3U\n\n""" + qualities = [144, 240, 360, 480, 720, 1080] + file = [] + for quality in qualities: + if quality < height: + newWidth = int(quality) + newHeight = int(float(width) / float(height) * newWidth) + if (newHeight % 2) != 0: + newHeight += 1 + m3u8Line = f"""#EXT-X-STREAM-INF:BANDWIDTH={newWidth*newWidth*1000},RESOLUTION={newHeight}x{newWidth}\n/other-video/{quality}/{otherHash}\n""" + file.append(m3u8Line) + lastLine = f"#EXT-X-STREAM-INF:BANDWIDTH={width*height*1000},RESOLUTION={width}x{height}\n/other-video/{otherHash}\n" + file.append(lastLine) + file = file[::-1] + file = "".join(file) + m3u8File += file + response = make_response(m3u8File) + + response.headers.set("Content-Type", "application/x-mpegURL") + response.headers.set("Range", "bytes=0-4095") + response.headers.set("Accept-Encoding", "*") + response.headers.set("Access-Control-Allow-Origin", f"http://{local_ip}:{serverPort}") + response.headers.set( + "Content-Disposition", "attachment", filename=f"{otherHash}.m3u8" + ) + return response + def generateCaptionSerie(episodeId): episode = Episodes.query.filter_by(episodeId=episodeId).first() season = Seasons.query.filter_by(seasonId=episode.seasonId).first() @@ -2995,17 +3361,10 @@ def generateCaptionSerie(episodeId): if subtitleType.lower() != "pgs": allCaptions.append( { - "index": index, - "languageCode": language, - "language": newLanguage, - "url": f"/chunkCaptionSerie/{language}/{index}/{episodeId}.vtt", - "name": titleName, - } + "index": index, "languageCode": language, "language": newLanguage, "url": f"/chunkCaptionSerie/{language}/{index}/{episodeId}.vtt", "name": titleName, } ) return allCaptions - - def generateCaptionMovie(movieID): movie = Movies.query.filter_by(id=movieID).first() library = movie.libraryName @@ -3053,12 +3412,7 @@ def generateCaptionMovie(movieID): titleName = f"{newLanguage} Full" allCaptions.append( { - "index": index, - "languageCode": language, - "language": newLanguage, - "url": f"/chunkCaption/vtt/{index}/{movieID}.vtt", - "name": titleName, - } + "index": index, "languageCode": language, "language": newLanguage, "url": f"/chunkCaption/vtt/{index}/{movieID}.vtt", "name": titleName, } ) return allCaptions @@ -3093,6 +3447,8 @@ def generateAudioMovie(movieID): index = line.split(",")[0] try: title = line.split(",")[2] + if " " in title: + title = language except: title = language default = "YES" if index == "1" else "NO" @@ -3430,12 +3786,7 @@ def sort_dict_by_key(unsorted_dict): if enabledRPC == "true": try: RPC.update( - state="Loading Chocolate...", - details=f"The Universal MediaManager | v{chocolateVersion} ({lastCommitHash})", - large_image="loader", - large_text="Chocolate", - buttons=[{"label": "Github", "url": "https://github.com/ChocolateApp/Chocolate"}], - start=start_time) + state="Loading Chocolate...", details=f"The Universal MediaManager | v{chocolateVersion} ({lastCommitHash})", large_image="loader", large_text="Chocolate", buttons=[{"label": "Github", "url": "https://github.com/ChocolateApp/Chocolate"}], start=start_time) except: pass @@ -3444,16 +3795,15 @@ def sort_dict_by_key(unsorted_dict): libraries = [library.__dict__ for library in libraries] libraries = sorted(libraries, key=lambda k: k['libName']) libraries = sorted(libraries, key=lambda k: k['libType']) - try: - for library in libraries: - if library["libType"] == "series": - getSeries(library["libName"]) - elif library["libType"] == "movies": - getMovies(library["libName"]) - elif library["libType"] == "games": - getGames(library["libName"]) - except: - pass + for library in libraries: + if library["libType"] == "series": + getSeries(library["libName"]) + elif library["libType"] == "movies": + getMovies(library["libName"]) + elif library["libType"] == "games": + getGames(library["libName"]) + elif library["libType"] == "other": + getOthersVideos(library["libName"]) print() print("\033[?25h", end="") @@ -3462,12 +3812,7 @@ def sort_dict_by_key(unsorted_dict): if enabledRPC == "true": try: RPC.update( - state="Idling", - details=f"The Universal MediaManager | v{chocolateVersion} ({lastCommitHash})", - large_image="largeimage", - large_text="Chocolate", - buttons=[{"label": "Github", "url": "https://github.com/ChocolateApp/Chocolate"}], - start=time.time()) + state="Idling", details=f"The Universal MediaManager | v{chocolateVersion} ({lastCommitHash})", large_image="largeimage", large_text="Chocolate", buttons=[{"label": "Github", "url": "https://github.com/ChocolateApp/Chocolate"}], start=time.time()) except: pass diff --git a/config.ini b/config.ini index bf117c8..ee8dc18 100644 --- a/config.ini +++ b/config.ini @@ -1,5 +1,5 @@ [ChocolateSettings] -version = 5.4.0 +version = 5.5.0 localip = Empty port = 8500 language = Empty diff --git a/intro.py b/intro.py index 9ade04c..bd32668 100644 --- a/intro.py +++ b/intro.py @@ -1,21 +1,41 @@ -import configparser, os, re, cv2, imagehash + +import configparser, os, re, cv2, imagehash, time, colorama from PIL import Image from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) config = configparser.ConfigParser() dir = os.path.dirname(__file__) -config.read(os.path.join(dir, 'config.ini')) +config.read(os.path.join(dir, "config.ini")) + +colorama.init() dirPath = os.path.dirname(__file__) -app.config["SQLALCHEMY_DATABASE_URI"] = f'sqlite:///{dirPath}/database.db' +app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{dirPath}/database.db" app.config['MAX_CONTENT_LENGTH'] = 4096 * 4096 app.config['UPLOAD_FOLDER'] = f"{dirPath}/static/img/" -app.config["SECRET_KEY"] = "ChocolateDBPassword" -app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +app.config['SECRET_KEY'] = "ChocolateDBPassword" +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) +class Libraries(db.Model): + libName = db.Column(db.String(255), primary_key=True) + libImage = db.Column(db.String(255)) + libType = db.Column(db.String(255)) + libFolder = db.Column(db.Text) + availableFor = db.Column(db.Text) + + def __init__(self, libName, libImage, libType, libFolder, availableFor): + self.libName = libName + self.libImage = libImage + self.libType = libType + self.libFolder = libFolder + self.availableFor = availableFor + + def __repr__(self) -> str: + return f"" + class Series(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), primary_key=True) @@ -30,8 +50,10 @@ class Series(db.Model): note = db.Column(db.String(255)) date = db.Column(db.String(255)) serieModifiedTime = db.Column(db.Float) + libraryName=db.Column(db.String(255)) + adult = db.Column(db.String(255)) - def __init__(self, id, name, originalName, genre, duration, description, cast, bandeAnnonceUrl, serieCoverPath, banniere, note, date, serieModifiedTime): + def __init__(self, id, name, originalName, genre, duration, description, cast, bandeAnnonceUrl, serieCoverPath, banniere, note, date, serieModifiedTime, adult, libraryName): self.id = id self.name = name self.originalName = originalName @@ -45,10 +67,41 @@ def __init__(self, id, name, originalName, genre, duration, description, cast, b self.note = note self.date = date self.serieModifiedTime = serieModifiedTime + self.libraryName = libraryName + self.adult = adult def __repr__(self) -> str: return f"" + +class Seasons(db.Model): + + serie = db.Column(db.Integer, nullable=False) + seasonId = db.Column(db.Integer, primary_key=True) + seasonNumber = db.Column(db.Integer, primary_key=True) + release = db.Column(db.String(255)) + episodesNumber = db.Column(db.String(255)) + seasonName = db.Column(db.String(255)) + seasonDescription = db.Column(db.Text) + seasonCoverPath = db.Column(db.String(255)) + modifiedDate = db.Column(db.Float) + numberOfEpisodeInFolder = db.Column(db.Integer) + + def __init__(self, serie, release, episodesNumber, seasonNumber, seasonId, seasonName, seasonDescription, seasonCoverPath, modifiedDate, numberOfEpisodeInFolder): + self.serie = serie + self.release = release + self.episodesNumber = episodesNumber + self.seasonNumber = seasonNumber + self.seasonId = seasonId + self.seasonName = seasonName + self.seasonDescription = seasonDescription + self.seasonCoverPath = seasonCoverPath + self.modifiedDate = modifiedDate + self.numberOfEpisodeInFolder = numberOfEpisodeInFolder + + def __repr__(self) -> str: + return f"" + class Episodes(db.Model): seasonId = db.Column(db.Integer, nullable=False) episodeId = db.Column(db.Integer, primary_key=True) @@ -75,44 +128,52 @@ def __init__(self, episodeId, episodeName, seasonId, episodeNumber, episodeDescr def __repr__(self) -> str: return f"" + with app.app_context(): db.init_app(app) def listAllSeasons(): - series = Series.query.all() - seriesPath = config["ChocolateSettings"]["seriespath"] - seriesIntro = {} - for serie in series: - serie = serie.__dict__ - del serie["_sa_instance_state"] - seriePath = f"{seriesPath}/{serie['originalName']}" - allSeasonData = {} - for seasons in os.listdir(seriePath): - seasonPath = f"{seriePath}/{seasons}" - allEpisodesData = detectIntro(seasonPath) - allSeasonData[seasons] = allEpisodesData - seriesIntro[serie["originalName"]] = allSeasonData - -def detectIntro(mainPath): + libraries = Libraries.query.all() + for library in libraries: + libType = library.libType + if libType == "series": + seriesPath = library.libFolder + series = Series.query.filter_by(libraryName=library.libName).all() + seriesIntro = {} + for serie in series: + serie = serie.__dict__ + del serie['_sa_instance_state'] + seriePath2 = f"{seriesPath}/{serie['originalName']}" + allSeasonData = {} + for seasons in os.listdir(seriePath2): + seasonPath = f"{seriePath2}/{seasons}" + findPath = seasonPath.replace(seriesPath, "") + print(f"Detecing intro for {serie['originalName']} {seasons} ") + allEpisodesData = detectIntro(seasonPath, library.libName, findPath, serie['originalName']) + allSeasonData[seasons] = allEpisodesData + + +def detectIntro(mainPath, libName, findPath, seasonName): episodesListe1 = listAllVideoFiles(mainPath) - episodesListe1.sort(key=lambda f: int(re.sub('\D', '', f))) + episodesListe1.sort(key=lambda f: int(re.sub("\D", "", f))) episodesData = {} + librarie = Libraries.query.filter_by(libName=libName).first() + seriesPath = librarie.libFolder for episode1 in episodesListe1: episodeSlug = f"{mainPath}/{episode1}".replace("\\", "/") - seriesPath = config["ChocolateSettings"]["seriespath"].replace("\\", "/") + episodeFindPath = f"{findPath}/{episode1}".replace("\\", "/") episodeSlug = episodeSlug.replace(seriesPath, "") - episodeData = Episodes.query.filter_by(slug=episodeSlug).first() - a = bool(episodeData.introStart == 0.0) # if introStart is 0.0, then it's not set - b = bool(episodeData.introEnd == 0.0) # if introEnd is 0.0, then it's not set - c = bool(a and b) # if both are 0.0, then it's not set - #print(f"Pour l'épisode {episodeSlug}, a = {a}, b = {b}, c = {c}") + episodeData = Episodes.query.filter_by(slug=episodeFindPath).first() + a = bool(episodeData.introStart == 0.0) # if introStart is 0.0, then it"s not set + b = bool(episodeData.introEnd == 0.0) # if introEnd is 0.0, then it"s not set + c = bool(a and b) # if both are 0.0, then it"s not set + #print(f"Pour l"épisode {episodeSlug}, a = {a}, b = {b}, c = {c}") if (a ^ b) or c: - print(f"Detecting intro for {episode1}, {episodeSlug}") episodeOneData = {} - episodeOneData["name"] = episode1 - episodeOneData["start"] = None - episodeOneData["end"] = None + episodeOneData['name'] = episode1 + episodeOneData['start'] = None + episodeOneData['end'] = None episode1Index = episodesListe1.index(episode1) if episode1Index < len(episodesListe1) - 1: episode2 = episodesListe1[episode1Index + 1] @@ -134,8 +195,6 @@ def detectIntro(mainPath): maxFrame1 = round(frameRate1 * 60*5) maxFrame2 = round(frameRate2 * 60*5) - #keep only the first 180 seconds of the video - #print(f"Serie: {mainPath}") #print(f"videoName: {episode1} - maxFrame1: {maxFrame1} - frameCount1: {frameCount1} - frameRate1: {frameRate1}\nvideoName: {episode2} - maxFrame2: {maxFrame2} - frameCount2: {frameCount2} - frameRate2: {frameRate2}") @@ -143,50 +202,61 @@ def detectIntro(mainPath): secondesEp1 = [] ep1Start = 0 secondesEp2 = [] + ratio = (int(160/2), int(90/2)) for frameEp1 in range(1, maxFrame1, round(frameRate1)): cap1.set(1, frameEp1) ret1, frame1 = cap1.read() - ratio1 = (160, 90) - frame1 = cv2.resize(frame1, ratio1, interpolation = cv2.INTER_LINEAR) - frameHash1 = str(imagehash.dhash(Image.fromarray(frame1))) - secondesEp1.append(frameHash1) - #print(f"frameEp1: {str(frameEp1).zfill(4)} - frame1: {frameHash1}") + frame1 = cv2.resize(frame1, ratio, interpolation = cv2.INTER_LINEAR) + frame1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY) + frame1 = str(imagehash.dhash(Image.fromarray(frame1))) + secondesEp1.append(frame1) + #print(f"frameEp1: {str(frameEp1).zfill(4)} - frame1: {frame1}") for frameEp2 in range(1,maxFrame2, round(frameRate2)): cap2.set(1, frameEp2) ret1, frame2 = cap2.read() - ratio2 = (160, 90) - frame2 = cv2.resize(frame2, ratio2, interpolation = cv2.INTER_LINEAR) - frameHash2 = str(imagehash.dhash(Image.fromarray(frame2))) - secondesEp2.append(frameHash2) - #print(f"frameEp2: {str(frameEp2).zfill(4)} - frame2: {frameHash2}") + frame2 = cv2.resize(frame2, ratio, interpolation = cv2.INTER_LINEAR) + frame2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY) + frame2 = str(imagehash.dhash(Image.fromarray(frame2))) + secondesEp2.append(frame2) + #print(f"frameEp2: {str(frameEp2).zfill(4)} - frame2: {frame2}") while searchForIntroStart: for frame1 in secondesEp1: #print(f"Searching {frame1} in secondesEp2") if frame1 in secondesEp2: ep1StartIndex = secondesEp1.index(frame1) ep1Start = ep1StartIndex - episodeOneData["start"] = ep1Start + episodeOneData['start'] = ep1Start ep2StartIndex = secondesEp2.index(frame1) searchForIntroStart = False break - #print(f"J'ai trouvé le départ de l'intro à {ep1Start}s dans {episode1} et {ep2StartIndex}s dans {episode2}") + #print(f"J"ai trouvé le départ de l"intro à {ep1Start}s dans {episode1} et {ep2StartIndex}s dans {episode2}") secondesEp1 = secondesEp1[ep1StartIndex:] secondesEp2 = secondesEp2[ep2StartIndex:] #print(secondesEp1) + for frame in secondesEp1: secondes = secondesEp1.index(frame) if frame in secondesEp2: - episodeOneData["end"] = secondes - elif secondes genreChecked[key] == true) + for (const movie of allMovies) { + work = true + for (genre of allGenreCheckeds) { + allGenresOfMovie = movie.getAttribute("data-genre") + if (allGenresOfMovie.includes(genre) == false) { + work = false + } + } + if (work == true) { + movie.style.display = "block" + } else { + movie.style.display = "none" + } + } } function getFirstMovies() { movies = document.getElementsByClassName("movies")[0] routeToUse = movies.getAttribute("id") movies.id = "movies" + let allGenres = [] let username = "" fetch("/whoami").then(function(response) { return response.json() @@ -284,12 +331,47 @@ function getFirstMovies() { let movieID = movie.id let cover = document.createElement("div") cover.className = "cover" + cover.setAttribute("data-id", movieID) + cover.setAttribute("data-title", movie.title) + cover.setAttribute("data-rating", movie.note) + let genres = movie.genre + genres = createObjectFromString(genres) + for (let genre of genres) { + if (cover.getAttribute("data-genre") !== null) { + cover.setAttribute("data-genre", `${cover.getAttribute("data-genre")} ${genre}`) + + } else { + cover.setAttribute("data-genre", `${genre}`) + } + + if (allGenres.includes(genre) === false) { + allGenres.push(genre) + } + } + let dateString = movie.date + const dateParts = dateString.split("/"); + const day = parseInt(dateParts[0], 10); + const month = parseInt(dateParts[1], 10); + const year = parseInt(dateParts[2], 10); + const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let daysSinceStartOfYear = 0; + + for (let i = 0; i < month - 1; i++) { + daysSinceStartOfYear += daysInMonth[i]; + } + + daysSinceStartOfYear += day; + const daysSinceStartOfTime = (year * 365) + daysSinceStartOfYear; + + cover.setAttribute("data-date", daysSinceStartOfTime) + cover.style.marginBottom = "2vh" let content = document.createElement("div") content.className = "content" let image = document.createElement("img") image.className = "cover_movie" image.src = movie.cover + image.setAttribute("loading", "lazy"); if (image.src == "https://image.tmdb.org/t/p/originalNone") { image.src = brokenPath } @@ -409,8 +491,6 @@ function getFirstMovies() { } } - removeLoader(data) - if (data.length == 1) { let bigBackground = document.getElementsByClassName("bannerCover")[0] bigBackground.style.height = "100vh" @@ -419,18 +499,196 @@ function getFirstMovies() { let bannerTitle = document.getElementsByClassName("bannerTitle")[0] let bannerDescription = document.getElementsByClassName("bannerDescription")[0] let watchNow = document.getElementsByClassName("watchNowA")[0] + let downloadA = document.getElementById("downloadNowA") + let rescanButton = document.getElementById("rescanButton") + + let selectA = document.getElementsByClassName("selectA")[0] + let sortA = document.getElementsByClassName("sortA")[0] bannerGenre.style.top = "46vh" bannerTitle.style.top = "47.5vh" bannerDescription.style.top = "55vh" watchNow.style.top = "65vh" + downloadA.style.top = "65vh" + rescanButton.style.marginTop = "65vh" + + selectA.style.display = "none" + sortA.style.display = "none" } + + hideLoader(data) + //order allGenres by alphabetical order + allGenres.sort() + for (const genre of allGenres) { + let checkboxes = document.getElementById("checkboxes") + let checkbox = document.createElement("input") + checkbox.setAttribute("type", "checkbox") + + checkbox.setAttribute("id", genre) + checkbox.setAttribute("name", genre) + checkbox.setAttribute("value", genre) + checkbox.setAttribute("class", "checkboxGenre") + + let label = document.createElement("label") + label.setAttribute("for", genre) + label.appendChild(checkbox) + label.innerHTML += `${genre}` + checkboxes.appendChild(label) + } setPopup() + setGenreCheckboxes() }) }) } +var expanded = false; + +function showCheckboxes() { + var checkboxes = document.getElementById("checkboxes"); + let select = document.querySelector(".selectBox select") + let selectA = document.getElementsByClassName("selectA")[0] + if (!expanded) { + checkboxes.style.display = "block"; + selectA.style.background = "white" + select.style.color = "black" + expanded = true; + } else { + checkboxes.style.display = "none"; + selectA.style.background = "none" + select.style.color = "white" + expanded = false; + } +} + +function forceHideLoader() { + let loaderBackground = document.getElementById("loaderBackground") + loaderBackground.style.display = "none" + let spinner = document.getElementsByClassName("spinner")[0] + spinner.style.display = "none" + spinner.style.opacity = "0" +} + +let oldSelectValue = "title" +let alreadySorted = false + +function sortFilms() { + let select = document.getElementById("sortSelect") + let selectValue = select.value + if (selectValue == "title") { + orderByAlphabetical() + } else if (selectValue == "date") { + orderByDate() + } else if (selectValue == "rating") { + orderByRating() + } + select.options[0].innerText = select.options[select.selectedIndex].label; + select.selectedIndex = 0; +} + +function orderByAlphabetical() { + showLoader() + let allMovies = document.getElementById("movies") + let movies = allMovies.children + let moviesArray = [] + for (let i = 0; i < movies.length; i++) { + moviesArray.push(movies[i]) + } + moviesArray.sort(function(a, b) { + let titleA = a.getAttribute("data-title") + let titleB = b.getAttribute("data-title") + if (titleA < titleB) { + return -1 + } else if (titleA > titleB) { + return 1 + } + return 0 + }) + allMovies.innerHTML = "" + if (oldSelectValue == "title") { + moviesArray.reverse() + oldSelectValue = "titleReverse" + } else { + oldSelectValue = "title" + } + + for (let i = 0; i < moviesArray.length; i++) { + allMovies.appendChild(moviesArray[i]) + } + + forceHideLoader() +} + +function orderByRating() { + showLoader() + let allMovies = document.getElementById("movies") + let movies = allMovies.children + let moviesArray = [] + for (let i = 0; i < movies.length; i++) { + moviesArray.push(movies[i]) + } + moviesArray.sort(function(a, b) { + let ratingA = a.getAttribute("data-rating") + let ratingB = b.getAttribute("data-rating") + if (ratingA < ratingB) { + return 1 + } else if (ratingA > ratingB) { + return -1 + } else { + return 0 + } + }) + if (oldSelectValue == "rating") { + moviesArray.reverse() + oldSelectValue = "ratingReverse" + } else { + oldSelectValue = "rating" + } + + + allMovies.innerHTML = "" + for (let i = 0; i < moviesArray.length; i++) { + allMovies.appendChild(moviesArray[i]) + } + forceHideLoader() +} + +function orderByDate() { + showLoader() + let allMovies = document.getElementById("movies") + let movies = allMovies.children + let moviesArray = [] + for (let i = 0; i < movies.length; i++) { + moviesArray.push(movies[i]) + } + moviesArray.sort(function(a, b) { + let dateA = a.getAttribute("data-date") + let dateB = b.getAttribute("data-date") + if (dateA < dateB) { + return 1 + } else if (dateA > dateB) { + return -1 + } else { + return 0 + } + }) + + if (oldSelectValue == "date") { + moviesArray.reverse() + oldSelectValue = "dateReverse" + } else { + oldSelectValue = "date" + } + + + + allMovies.innerHTML = "" + for (let i = 0; i < moviesArray.length; i++) { + allMovies.appendChild(moviesArray[i]) + } + forceHideLoader() +} + window.onload = function() { brokenPathDiv = document.getElementsByClassName("brokenPath")[0] brokenPath = brokenPathDiv.getAttribute("id") diff --git a/static/js/allSeries.js b/static/js/allSeries.js index 0790e36..2ff04f5 100644 --- a/static/js/allSeries.js +++ b/static/js/allSeries.js @@ -448,12 +448,25 @@ function setPopup() { }) } +let expanded = false; + +function showCheckboxes() { + var checkboxes = document.getElementById("checkboxes"); + if (!expanded) { + checkboxes.style.display = "block"; + expanded = true; + } else { + checkboxes.style.display = "none"; + expanded = false; + } + } function getFirstSeries() { series = document.getElementsByClassName("series")[0] routeToUse = series.getAttribute("id") series.id = "series" + let allGenres = [] fetch(routeToUse).then(function(response) { return response.json() @@ -496,6 +509,42 @@ function getFirstSeries() { series = document.getElementsByClassName("series")[0] var cover = document.createElement("div") cover.className = "cover" + + let genres = serie[1]["genre"] + genres = JSON.parse(genres) + for (let genre of genres) { + if (cover.getAttribute("data-genre") !== null) { + cover.setAttribute("data-genre", `${cover.getAttribute("data-genre")} ${genre}`) + + } else { + cover.setAttribute("data-genre", `${genre}`) + } + + if (allGenres.includes(genre) === false) { + allGenres.push(genre) + } + } + + cover.setAttribute("data-title", serie[1]["name"]) + cover.setAttribute("data-rating", serie[1]["note"]) + let dateString = serie[1]["date"]; + const dateParts = dateString.split("-"); + const day = parseInt(dateParts[2], 10); + const month = parseInt(dateParts[1], 10); + const year = parseInt(dateParts[0], 10); + const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + let daysSinceStartOfYear = 0; + + for (let i = 0; i < month - 1; i++) { + daysSinceStartOfYear += daysInMonth[i]; + } + + daysSinceStartOfYear += day; + const daysSinceStartOfTime = (year * 365) + daysSinceStartOfYear; + + cover.setAttribute("data-date", daysSinceStartOfTime) + + var content = document.createElement("div") content.className = "content" var image = document.createElement("img") @@ -508,6 +557,7 @@ function getFirstSeries() { image.title = serie[0] image.alt = serie[0] image.setAttribute("serieId", serie[1]['id'].toString()) + image.setAttribute("loading", "lazy"); content.appendChild(image) cover.appendChild(content) @@ -515,36 +565,224 @@ function getFirstSeries() { } } - if (data.length <= 1) { - spinner = document.getElementsByClassName("spinner")[0] - backgroundSpinner = document.getElementById("loaderBackground") - spinner.style.opacity = "0" - spinner.style.display = "none" - backgroundSpinner.style.display = "none" - } else { + - const imgs = document.images - const imgsArray = Array.prototype.slice.call(document.images) - - for (img of imgsArray) { - const acutalIndex = imgsArray.indexOf(img) - img = imgs.item(acutalIndex) - img.addEventListener("load", function() { - const imagesLenght = imgs.length - 1 - if (acutalIndex == imagesLenght) { - spinner = document.getElementsByClassName("spinner")[0] - backgroundSpinner = document.getElementById("loaderBackground") - spinner.style.opacity = "0" - spinner.style.display = "none" - backgroundSpinner.style.display = "none" - } - }) - }} + if (data.length == 1) { + let bigBackground = document.getElementsByClassName("bannerCover")[0] + bigBackground.style.height = "100vh" + + let bannerGenre = document.getElementsByClassName("bannerGenre")[0] + let bannerTitle = document.getElementsByClassName("bannerTitle")[0] + let bannerDescription = document.getElementsByClassName("bannerDescription")[0] + let watchNow = document.getElementsByClassName("watchNowA")[0] + let rescanButton = document.getElementById("rescanButton") + + let selectA = document.getElementsByClassName("selectA")[0] + let sortA = document.getElementsByClassName("sortA")[0] + + bannerGenre.style.top = "46vh" + bannerTitle.style.top = "47.5vh" + bannerDescription.style.top = "55vh" + watchNow.style.top = "65vh" + rescanButton.style.marginTop = "65vh" + + selectA.style.display = "none" + sortA.style.display = "none" + } + + + allGenres.sort() + for (const genre of allGenres) { + let checkboxes = document.getElementById("checkboxes") + let checkbox = document.createElement("input") + checkbox.setAttribute("type", "checkbox") + + checkbox.setAttribute("id", genre) + checkbox.setAttribute("name", genre) + checkbox.setAttribute("value", genre) + checkbox.setAttribute("class", "checkboxGenre") + + let label = document.createElement("label") + label.setAttribute("for", genre) + label.appendChild(checkbox) + + label.innerHTML += `${genre}` + checkboxes.appendChild(label) + } + + removeLoader(data) setPopup() }) } +function showLoader() { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "1" + spinner.style.display = "block" + backgroundSpinner.style.display = "block" +} + +function forceHideLoader() { + let loaderBackground = document.getElementById("loaderBackground") + loaderBackground.style.display = "none" + let spinner = document.getElementsByClassName("spinner")[0] + spinner.style.display = "none" + spinner.style.opacity = "0" +} + +let oldSelectValue = "title" +let alreadySorted = false + +function sortFilms() { + let select = document.getElementById("sortSelect") + let selectValue = select.value + if (selectValue == "title") { + orderByAlphabetical() + } else if (selectValue == "date") { + orderByDate() + } else if (selectValue == "rating") { + orderByRating() + } + select.options[0].innerText = select.options[select.selectedIndex].label; + select.selectedIndex = 0; +} + +function orderByAlphabetical() { + showLoader() + let allMovies = document.getElementById("series") + let movies = allMovies.children + let moviesArray = [] + for (let i = 0; i < movies.length; i++) { + moviesArray.push(movies[i]) + } + moviesArray.sort(function(a, b) { + let titleA = a.getAttribute("data-title") + let titleB = b.getAttribute("data-title") + if (titleA < titleB) { + return -1 + } else if (titleA > titleB) { + return 1 + } + return 0 + }) + allMovies.innerHTML = "" + if (oldSelectValue == "title") { + moviesArray.reverse() + oldSelectValue = "titleReverse" + } else { + oldSelectValue = "title" + } + + for (let i = 0; i < moviesArray.length; i++) { + allMovies.appendChild(moviesArray[i]) + } + + forceHideLoader() +} + +function orderByRating() { + showLoader() + let allMovies = document.getElementById("series") + let movies = allMovies.children + let moviesArray = [] + for (let i = 0; i < movies.length; i++) { + moviesArray.push(movies[i]) + } + moviesArray.sort(function(a, b) { + let ratingA = a.getAttribute("data-rating") + let ratingB = b.getAttribute("data-rating") + if (ratingA < ratingB) { + return 1 + } else if (ratingA > ratingB) { + return -1 + } else { + return 0 + } + }) + if (oldSelectValue == "rating") { + moviesArray.reverse() + oldSelectValue = "ratingReverse" + } else { + oldSelectValue = "rating" + } + + + allMovies.innerHTML = "" + for (let i = 0; i < moviesArray.length; i++) { + allMovies.appendChild(moviesArray[i]) + } + forceHideLoader() +} + +function orderByDate() { + showLoader() + let allMovies = document.getElementById("series") + let movies = allMovies.children + let moviesArray = [] + for (let i = 0; i < movies.length; i++) { + moviesArray.push(movies[i]) + } + moviesArray.sort(function(a, b) { + let dateA = a.getAttribute("data-date") + let dateB = b.getAttribute("data-date") + if (dateA < dateB) { + return 1 + } else if (dateA > dateB) { + return -1 + } else { + return 0 + } + }) + + if (oldSelectValue == "date") { + moviesArray.reverse() + oldSelectValue = "dateReverse" + } else { + oldSelectValue = "date" + } + + + + allMovies.innerHTML = "" + for (let i = 0; i < moviesArray.length; i++) { + allMovies.appendChild(moviesArray[i]) + } + forceHideLoader() +} + +function removeLoader(data){ + if (data.length <= 1) { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "0" + spinner.style.display = "none" + backgroundSpinner.style.display = "none" + } else { + + const imgs = document.images + const imgsArray = Array.prototype.slice.call(document.images) + imgs.length = 32 + imgsArray.splice(36, imgsArray.length - 1) + imgsArray.splice(0, 4) + for (img of imgsArray) { + const acutalIndex = imgsArray.indexOf(img) + img = imgs.item(acutalIndex) + img.addEventListener("load", function() { + const imagesLenght = imgsArray.length - 1 + if (acutalIndex == (imagesLenght-4)) { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "0" + spinner.style.display = "none" + backgroundSpinner.style.display = "none" + } + }) + }} +} + + window.onload = function() { brokenPathDiv = document.getElementsByClassName("brokenPath")[0] brokenPath = brokenPathDiv.getAttribute("id") diff --git a/static/js/channel.js b/static/js/channel.js index c54a4f1..93113a1 100644 --- a/static/js/channel.js +++ b/static/js/channel.js @@ -23,4 +23,12 @@ options = { } //add the quality selector -var player = videojs('movie', options); \ No newline at end of file +var player = videojs('movie', options); + +//when the player is ready, show the progress bar and set the visibility to hidden +player.ready(function() { + progressBar = document.querySelector('.vjs-progress-control'); + progressBar.style.visibility = 'hidden'; + progressBar.style.display = 'block'; + +}); \ No newline at end of file diff --git a/static/js/login.js b/static/js/login.js index ca68242..bab06ab 100644 --- a/static/js/login.js +++ b/static/js/login.js @@ -19,8 +19,9 @@ for (user of allusers) { document.getElementsByClassName("nameInputForm")[0].value = this.id loginForm = document.getElementsByClassName("loginForm")[0] accountType = this.getAttribute("type") + passwordEmpty = this.getAttribute("data-passwordEmpty") print - if (["Admin", "Adult", "Teen"].includes(accountType)) { + if (["Admin", "Adult", "Teen"].includes(accountType) && passwordEmpty == "no") { loginForm.style.display = "block" } else { loginForm = document.getElementsByClassName("loginForm")[0] diff --git a/static/js/main.js b/static/js/main.js index f4f4be1..915932e 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -48,4 +48,44 @@ if ('serviceWorker' in navigator) { window.addEventListener('online', function(e) { console.log("You are online"); -}, false); \ No newline at end of file +}, false); + +function rescanLib() { + button = document.getElementById("rescanButton") + url = button.getAttribute("data-url") + texts = ["Scanning", "Scanning.", "Scanning..", "Scanning..."] + button.disabled = true + + //setInterval + var i = 0 + var interval = setInterval(function() { + i++ + if (i == 4) { + i = 0 + } + button.innerHTML = `${texts[i]}` + }, 500) + + //fetch with get + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json" + }}).then(function(response) { + return response.json() + }).then(function(data) { + console.log(data) + if (data == true) { + clearInterval(interval) + button.innerHTML = 'Done' + //wait 2 seconds and reload the page + setTimeout(function() { + window.location.reload() + }, 2000) + } else { + clearInterval(interval) + button.innerHTML = 'Error' + button.classList.add("error") + } + }) +} \ No newline at end of file diff --git a/static/js/movie.js b/static/js/movie.js index 6e51cbe..b9d9223 100644 --- a/static/js/movie.js +++ b/static/js/movie.js @@ -25,6 +25,11 @@ function getElementByXpath(path) { window.onload = function() { let lastPush = 0 let options; + let allSourceId = document.getElementById("allSourceId") + let allSourcesId = [] + for (let i = 0; i < allSourceId.children.length; i++) { + allSourcesId.push(allSourceId.children[i].id) + } var path = window.location.pathname movieID = window.location.href.split("/")[4] @@ -38,6 +43,11 @@ window.onload = function() { vhs: { overrideNative: !videojs.browser.IS_SAFARI, }, + nativeAudioTracks: false, + nativeVideoTracks: false + }, + plugins: { + chromecast: {} }, controlBar: { children: [ @@ -58,15 +68,82 @@ window.onload = function() { //add the quality selector var player = videojs('movie', options); + player.hlsQualitySelector({ displayCurrentQuality: true, placementIndex : 7 }); + var cc = new Castjs(); + var player = videojs('movie'); + + $(document).on("click", ".vjs-chromecast-button", () => { + if (cc.available && !cc.session) { + var vd = $("#my-video"); + cc.cast(vd.find("Source:first").attr("src"), { + poster: vd.attr("data-poster"), + title: vd.attr("data-title"), + time: player.currentTime(), + muted: false, + paused: false + }); + } else { + cc.disconnect(); + $(".cc-remote").hide(200); + $(".cc-state") + .find("use") + .attr("xlink:href", "#cc-inactive"); + } + }); + + cc.on("available", () => { + $( + '
' + ).insertAfter( + $(".vjs-control-bar") + .children() + .last() + ); + }); + + cc.on("session", () => { + $(".cc-remote").show(200); + $(".cc-state") + .find("use") + .attr("xlink:href", "#cc-active"); + $(".cc-remote-display-status-text").text("Casting to " + cc.device); + }); + + // + player.on("pause", () => { + if (cc.session) cc.pause(); + }); + + player.on("play", () => { + if (cc.session){ + player.pause(); + cc.play(); + } + }); + + player.on("seeking", () => { + if (cc.session) cc.seek(player.currentTime()); + }); + + player.on("volumechange", () => { + if (cc.session) { + if (player.muted()) { + cc.volume(0); + } else { + cc.volume(player.volume()); + } + } + }); + value = {false: "is not", true: "is"} console.log(`User ${value[videojs.browser.IS_IOS]} on IOS\nUser ${value[videojs.browser.IS_SAFARI]} on Safari\nUser ${value[videojs.browser.IS_ANDROID]} on Android\nUser ${value[videojs.browser.IS_CHROME]} on Chrome`) - + /* if (videojs.browser.IS_IOS || videojs.browser.IS_SAFARI) { player.airPlay(); let airPlayButton = getElementByXpath("//*[@id='movie']/div[4]/button[3]") @@ -75,7 +152,7 @@ window.onload = function() { player.chromecast(); let chromecastButton = getElementByXpath("//*[@id='movie']/div[4]/button[2]") chromecastButton.classList.remove("vjs-hidden") - } + }*/ var video = document.getElementById("movie_html5_api") diff --git a/static/js/other.js b/static/js/other.js new file mode 100644 index 0000000..b6022d8 --- /dev/null +++ b/static/js/other.js @@ -0,0 +1,166 @@ +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); +} + +function editMovie(title, library) { + window.location.href = `/editMovie/${title}/${library}` +} + +const createObjectFromString = (str) => { + return eval(`(function () { return ${str}; })()`); +} + +function svgEl(name, attrs) { + const el = document.createElementNS("http://www.w3.org/2000/svg", name) + for ( const [ k, v ] of Object.entries(attrs) ){ + el.setAttribute(k, v); + } + return el +} + +function removeLoader(){ + const imgs = document.images + const imgsArray = Array.prototype.slice.call(document.images) + imgs.length = 32 + imgsArray.splice(36, imgsArray.length - 1) + imgsArray.splice(0, 4) + if (imgsArray.length == 0) { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "0" + spinner.style.display = "none" + backgroundSpinner.style.display = "none" + } + + for (img of imgsArray) { + const acutalIndex = imgsArray.indexOf(img) + img = imgs.item(acutalIndex) + img.addEventListener("load", function() { + const imagesLenght = imgsArray.length - 1 + console.log(`${acutalIndex}/${imagesLenght-4}`) + if (acutalIndex == (imagesLenght-4)) { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "0" + spinner.style.display = "none" + backgroundSpinner.style.display = "none" + } + }) + } +} + +function getFirstMovies() { + movies = document.getElementsByClassName("movies")[0] + routeToUse = movies.getAttribute("id") + movies.id = "movies" + let username = "" + fetch("/whoami").then(function(response) { + return response.json() + }).then(function(data) { + username = data.name + accountType = data.accountType + }).then(function() { + fetch(routeToUse).then(function(response) { + return response.json() + }).then(function(data) { + for (let i = 0; i < data.length; i++) { + data[i] + if (i > 0) { + movies = document.getElementsByClassName("movies")[0] + let movie = data[i] + let movieID = movie.videoHash + let cover = document.createElement("div") + cover.className = "cover" + cover.style.marginBottom = "2vh" + let content = document.createElement("div") + content.className = "content" + let image = document.createElement("img") + image.className = "cover_movie" + image.src = movie.banner + if (image.src == "https://image.tmdb.org/t/p/originalNone") { + image.src = brokenPath + } + image.title = movie.title + image.alt = movie.title + image.setAttribute("data-id", movieID) + image.setAttribute("loading", "lazy") + + vues = movie.vues + + + vues = createObjectFromString(vues) + timeCode = vues[username] + let timeLineBackground = document.createElement("div") + timeLineBackground.className = "timeLineBackground" + let timeLine = document.createElement("div") + timeLine.className = "timeLine" + let watchedTime = vues[username] + let movieDuration = movie.duration + //it's a timecode, convert it to seconds + movieDuration = movieDuration.split(":") + movieDuration = parseInt(movieDuration[0]) * 3600 + parseInt(movieDuration[1]) * 60 + parseInt(movieDuration[2]) + if ((watchedTime / movieDuration) * 100 <= 100) { + timeLine.style.width = `${(watchedTime / movieDuration) * 100}%` + } else if ((watchedTime / movieDuration) * 100 > 100) { + timeLine.style.width = "100%" + } else { + timeLine.style.width = "0%" + } + timeLineBackground.appendChild(timeLine) + content.appendChild(timeLineBackground) + + cover.addEventListener("click", function() { + window.location.href = "/otherVideo/" + movieID + }) + + content.appendChild(image) + cover.appendChild(content) + movies.appendChild(cover) + + } else { + bigBanner = document.getElementsByClassName("bigBanner")[0] + imageBanner = document.getElementsByClassName("bannerCover")[0] + titleBanner = document.getElementsByClassName("bannerTitle")[0] + watchNow = document.getElementsByClassName("watchNowA")[0] + movie = data[i] + let id = movie.videoHash + slug = "/otherVideo/" + id + bannerImage = movie.banner + cssBigBanner = `background-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(24, 24, 24, 0.85) 77.08%, #1D1D1D 100%), linear-gradient(95.97deg, #000000 0%, rgba(0, 0, 0, 0.25) 100%, #000000 100%), url("${bannerImage}")` + imageBanner.setAttribute('style', cssBigBanner) + + titleBanner.innerHTML = movie.title + + movieUrl = slug + watchNow.setAttribute("href", movieUrl) + } + } + + removeLoader(data) + + if (data.length == 1) { + let bigBackground = document.getElementsByClassName("bannerCover")[0] + bigBackground.style.height = "100vh" + + let bannerTitle = document.getElementsByClassName("bannerTitle")[0] + let bannerDescription = document.getElementsByClassName("bannerDescription")[0] + let watchNow = document.getElementsByClassName("watchNowA")[0] + + bannerGenre.style.top = "46vh" + bannerTitle.style.top = "47.5vh" + bannerDescription.style.top = "55vh" + watchNow.style.top = "65vh" + } + }) + }) +} + +window.onload = function() { + brokenPathDiv = document.getElementsByClassName("brokenPath")[0] + brokenPath = brokenPathDiv.getAttribute("id") + brokenPathDiv.parentNode.removeChild(brokenPathDiv) + getFirstMovies() + removeLoader() +} \ No newline at end of file diff --git a/static/js/otherVideo.js b/static/js/otherVideo.js new file mode 100644 index 0000000..69b65a2 --- /dev/null +++ b/static/js/otherVideo.js @@ -0,0 +1,154 @@ +const createObjectFromString = (str) => { + return eval(`(function () { return ${str}; })()`); +} + +function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +function getElementByXpath(path) { + return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + } + +window.onload = function() { + let lastPush = 0 + let options; + + var path = window.location.pathname + movieID = window.location.href.split("/")[4] + + + options = { + controls: true, + preload: 'none', + techOrder: ['chromecast', 'html5'], + html5: { + vhs: { + overrideNative: !videojs.browser.IS_SAFARI, + }, + }, + controlBar: { + children: [ + 'playToggle', + 'volumePanel', + 'currentTimeDisplay', + 'progressControl', + 'remainingTimeDisplay', + 'captionsButton', + 'audioTrackButton', + 'chromecastButton', + 'airPlayButton', + 'pictureInPictureToggle', + 'fullscreenToggle', + ], + }, + } + + //add the quality selector + var player = videojs('movie', options); + player.hlsQualitySelector({ + displayCurrentQuality: true, + placementIndex : 7 + }); + + value = {false: "is not", true: "is"} + + console.log(`User ${value[videojs.browser.IS_IOS]} on IOS\nUser ${value[videojs.browser.IS_SAFARI]} on Safari\nUser ${value[videojs.browser.IS_ANDROID]} on Android\nUser ${value[videojs.browser.IS_CHROME]} on Chrome`) + + if (videojs.browser.IS_IOS || videojs.browser.IS_SAFARI) { + player.airPlay(); + let airPlayButton = getElementByXpath("//*[@id='movie']/div[4]/button[3]") + airPlayButton.classList.remove("vjs-hidden") + } else { + player.chromecast(); + let chromecastButton = getElementByXpath("//*[@id='movie']/div[4]/button[2]") + chromecastButton.classList.remove("vjs-hidden") + } + + + var video = document.getElementById("movie_html5_api") + video.addEventListener("timeupdate", function() { + let href = window.location.href + movieID = href.split("/")[4] + let currentTime = video.currentTime + currentTime = parseInt(currentTime) + if (currentTime == lastPush+1) { + fetch(`/setVuesOtherTimeCode/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + //set the form + body: JSON.stringify({ + movieHASH: movieID, + timeCode: currentTime + }) + }) + lastPush = currentTime + } + }) + + + let username = "" + fetch("/whoami").then(function(response) { + return response.json() + }).then(function(data) { + username = data.name + }).then(function() { + fetch(`/getOtherData/${movieID}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }).then(response => response.json()) + .then(data => { + vues = data.vues + //vues is a string representing an array convert it to an array + vues = createObjectFromString(vues) + if (vues[username] !== undefined){ + timeCode = vues[username] + timeCode = parseInt(timeCode) + var popup = document.getElementById("popup") + popup.style.display = "block" + + buttonYes = document.getElementById("buttonYes") + buttonYes.addEventListener("click", function() { + popup.style.display = "none" + document.body.style.overflow = "auto" + video = document.getElementById("movie_html5_api") + video.currentTime = timeCode + lastPush = timeCode + try { + video.play() + } catch (error) { + console.log(error) + } + }) + + buttonNo = document.getElementById("buttonNo") + buttonNo.addEventListener("click", function() { + popup.style.display = "none" + document.body.style.overflow = "auto" + video = document.getElementById("movie_html5_api") + }) + } + }) + }) + + var path = window.location.pathname + var slug = path.split("/") + slug = slug[2] + +} \ No newline at end of file diff --git a/static/js/serie.js b/static/js/serie.js index 640064b..4feadb9 100644 --- a/static/js/serie.js +++ b/static/js/serie.js @@ -13,15 +13,17 @@ window.onload = function() { }, controlBar: { children: [ - 'playToggle', - 'volumePanel', - 'currentTimeDisplay', - 'progressControl', - 'remainingTimeDisplay', - 'captionsButton', - 'audioTrackButton', - 'pictureInPictureToggle', - 'fullscreenToggle', + 'playToggle', + 'volumePanel', + 'currentTimeDisplay', + 'progressControl', + 'remainingTimeDisplay', + 'captionsButton', + 'audioTrackButton', + 'chromecastButton', + 'airPlayButton', + 'pictureInPictureToggle', + 'fullscreenToggle', ], }, } @@ -30,7 +32,6 @@ window.onload = function() { displayCurrentQuality: true, placementIndex: 7 }); - player.chromecast(); var video = document.getElementById("movie_html5_api") let introStart = 0 diff --git a/static/js/settings.js b/static/js/settings.js index c51e990..ae8c475 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -16,9 +16,15 @@ function saveSettings(event) { } function createAccount(event) { - form = document.getElementById("createAccount") - form.action = "/createAccount" - form.submit() + type = document.getElementById("type").value + password = document.getElementById("password").value + if (type == "Admind" && password == "") { + alert("You need to enter a password for an Admin account") + } else { + form = document.getElementById("createAccount") + form.action = "/createAccount" + form.submit() + } } function getCookie(name) { @@ -165,4 +171,40 @@ function newLib(){ } }) } +} + +function rescanAll() { + url = "/rescanAll" + button = document.getElementById("rescanAllButton") + texts = ["Scanning", "Scanning.", "Scanning..", "Scanning..."] + button.disabled = true + + //setInterval + var i = 0 + var interval = setInterval(function() { + i++ + if (i == 4) { + i = 0 + } + button.innerHTML = `${texts[i]}` + }, 500) + + //fetch with get + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json" + }}).then(function(response) { + return response.json() + }).then(function(data) { + console.log(data) + if (data == true) { + clearInterval(interval) + button.innerHTML = 'Done' + } else { + clearInterval(interval) + button.innerHTML = 'Error' + button.classList.add("error") + } + }) } \ No newline at end of file diff --git a/static/js/tv.js b/static/js/tv.js index 85bfc98..ddc688b 100644 --- a/static/js/tv.js +++ b/static/js/tv.js @@ -3,8 +3,7 @@ function main() { library = url.split("/")[4] fetch(`/getChannels/${library}`).then(function(response) { return response.json(); - }).then(function(json) { - channels = json + }).then(function(channels) { for (channel of channels) { let channelRealName = encode_utf8(channel.name).replace(/\(.*?\)|\[.*?\]/g, "") let channelID = channel.channelID @@ -14,6 +13,7 @@ function main() { let channelImage = document.createElement("img") channelImage.className = "channelImage" channelImage.src = channel.logo + channelImage.setAttribute("loading", "lazy") let channelName = document.createElement("p") channelName.className = "channelName" channelName.innerHTML = channelRealName @@ -24,9 +24,42 @@ function main() { }) document.getElementById("tvChannels").appendChild(channelDiv) } + removeLoader(channels) }) } + +function removeLoader(data){ + if (data.length <= 1) { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "0" + spinner.style.display = "none" + backgroundSpinner.style.display = "none" + } else { + + const imgs = document.images + const imgsArray = Array.prototype.slice.call(document.images) + imgs.length = 32 + imgsArray.splice(36, imgsArray.length - 1) + imgsArray.splice(0, 4) + for (img of imgsArray) { + const acutalIndex = imgsArray.indexOf(img) + img = imgs.item(acutalIndex) + img.addEventListener("load", function() { + const imagesLenght = imgsArray.length - 1 + //console.log(`${acutalIndex}/${imagesLenght-4}`) + if (acutalIndex == (imagesLenght-4)) { + spinner = document.getElementsByClassName("spinner")[0] + backgroundSpinner = document.getElementById("loaderBackground") + spinner.style.opacity = "0" + spinner.style.display = "none" + backgroundSpinner.style.display = "none" + } + }) + }} +} + function encode_utf8(s) { try { return decodeURIComponent(escape(s)); diff --git a/static/lang/languages.json b/static/lang/languages.json index 1ac310c..8af6692 100644 --- a/static/lang/languages.json +++ b/static/lang/languages.json @@ -2,57 +2,59 @@ "AF": { "movies": "Flieks", "series": "Reeks", - "consoles": "Konsolle", - "tvChannels": "TV Kanale", + "consoles": "Konsolegames", + "tvChannels": "TV-kanale", + "other": "Ander", "watchNow": "Kyk nou", "downloadButton": "Laai af", - "allowDownloads": "Laat aflaai toe", + "allowDownloads": "Laat aflaaie toe", "name": "Naam", "username": "Gebruikersnaam", "users": "Gebruikers", "password": "Wagwoord", "editProfile": "Wysig profiel", - "profilePic": "Profiel foto", - "accountType": "Rekening tipe", + "profilePic": "Profielfoto", + "accountType": "Rekenintipe", "admin": "Admin", - "adult": "Volwassene", - "teen": "Tienjarige", + "adult": "Volwasse", + "teen": "Tiener", "kid": "Kind", "path": "Pad", - "libraryName": "Biblioteek naam", - "libraryPath": "Biblioteek pad", + "libraryName": "Biblioteeknaam", + "libraryPath": "Biblioteekpad", "createAccount": "Skep rekening", - "selectMovieText1": "Kies die regte film vir ", + "selectMovieText1": "Kies die regte fliek vir ", "selectMovieText2": " in ", - "selectMovieSecondText": "As jy die film nie vind nie, probeer om die naam van die lêer te verander", - "customMovieId": "Aangepaste film ID", - "customIdExplication": "Om die film ID te kry, gaan na die film op TMDB en kopieer die nommers in die soekbalk", - "addThisMovie": "Voeg hierdie film by", - "continueWatch": "Wil jy voort", + "selectMovieSecondText": "As jy nie die fliek kan vind nie, probeer om die naam van die lêer te verander", + "customMovieId": "Aangepaste fliek-ID", + "customIdExplication": "Om die fliek-ID te kry, gaan na die fliek op TMDB en kopieer die syfers in die soekbalk", + "addThisMovie": "Voeg hierdie fliek by", + "continueWatch": "Wil jy verder kyk waar jy gestop het?", "yes": "Ja", "no": "Nee", "back": "Terug", - "previous": "Vorige", "next": "Volgende", - "gamesConsole": "xxconsoleNamexx Spelle", - "searchText": "Soek 'n film/reeks", - "offline": "Jy lyk asof jy aflyn", + "previous": "Vorige", + "gamesConsole": "xxconsoleNamexx-speletjies", + "searchText": "Soek na 'n fliek/reeks", + "offline": "Dit lyk of jy aflyn is", "languages": "Tale", "createNewLib": "Skep 'n nuwe biblioteek", - "serverPort": "Bedienerpo poort", - "tmdbApiKey": "TMDB API Sleutel", - "igdbIdKey": "IGDB ID Sleutel", - "igdbSecretKey": "IGDB Geheime Sleutel", + "serverPort": "Bedienerspoort", + "tmdbApiKey": "TMDB-API-sleutel", + "igdbIdKey": "IGDB-ID-sleutel", + "igdbSecretKey": "IGDB-geheime sleutel", "saveSettings": "Stoor instellings", "problemsWith": "Probleme met", - "dontUnderstand": "Ek verstaan nie", - "login": "Teken aan" + "dontUnderstand": "Ek begryp nie", + "login": "Teken in" }, "SQ": { "movies": "Filma", "series": "Seria", "consoles": "Konsola", "tvChannels": "Kanale TV", + "other": "Tjetër", "watchNow": "Shiko tani", "downloadButton": "Shkarko", "allowDownloads": "Lejo shkarkimet", @@ -102,6 +104,7 @@ "series": "የስሪያዎች", "consoles": "ኮንሰሎች", "tvChannels": "የቲቪ ክንውንቶች", + "other": "ሌሎች", "watchNow": "አሁን ይመልከቱ", "downloadButton": "ማውረድ", "allowDownloads": "የማውረድ ፍቃድ", @@ -151,6 +154,7 @@ "series": "مسلسلات", "consoles": "أجهزة", "tvChannels": "قنوات التلفزيون", + "other": "آخر", "watchNow": "شاهد الآن", "downloadButton": "تحميل", "allowDownloads": "السماح بالتنزيلات", @@ -200,6 +204,7 @@ "series": "Serialar", "consoles": "Konsollar", "tvChannels": "TV Kanalları", + "other": "Digər", "watchNow": "İndi izlə", "downloadButton": "Yüklə", "allowDownloads": "Yükləmələri icazə ver", @@ -249,6 +254,7 @@ "series": "Серыі", "consoles": "Кансолі", "tvChannels": "Тэлебачанні", + "other": "Іншыя", "watchNow": "Глядзець зараз", "downloadButton": "Спампаваць", "allowDownloads": "Дазволіць спампоўку", @@ -298,6 +304,7 @@ "series": "Сериали", "consoles": "Конзоли", "tvChannels": "Телевизионни канали", + "other": "Други", "watchNow": "Гледай сега", "downloadButton": "Изтегли", "allowDownloads": "Разреши изтеглянията", @@ -347,6 +354,7 @@ "series": "সিরিজ", "consoles": "কনসোল", "tvChannels": "টিভি চ্যানেল", + "other": "অন্যান্য", "watchNow": "এখন দেখুন", "downloadButton": "ডাউনলোড", "allowDownloads": "ডাউনলোড অনুমতি দিন", @@ -396,6 +404,7 @@ "series": "Serije", "consoles": "Konzole", "tvChannels": "TV Kanali", + "other": "Ostalo", "watchNow": "Gledaj sad", "downloadButton": "Preuzmi", "allowDownloads": "Dozvoli preuzimanje", @@ -444,6 +453,7 @@ "series": "Sèries", "consoles": "Consolas", "tvChannels": "Canals de TV", + "other": "Altres", "watchNow": "Veure ara", "downloadButton": "Descarregar", "allowDownloads": "Permetre descàrregues", @@ -492,6 +502,7 @@ "series": "Serie TV", "consoles": "Consoles", "tvChannels": "Canali TV", + "other": "Altro", "watchNow": "fighjate avà", "downloadButton": "Scarica", "allowDownloads": "Consenti i download", @@ -536,11 +547,111 @@ "dontUnderstand": "Non capisco", "login": "Accedi" }, + "CS": { + "movies": "Filmy", + "series": "Série", + "consoles": "Konzole", + "tvChannels": "TV kanály", + "other": "Jiné", + "watchNow": "Sledovat nyní", + "downloadButton": "Stáhnout", + "allowDownloads": "Povolit stahování", + "name": "Jméno", + "username": "Uživatelské jméno", + "users": "Uživatelé", + "password": "Heslo", + "editProfile": "Upravit profil", + "profilePic": "Profilový obrázek", + "accountType": "Typ účtu", + "admin": "Správce", + "adult": "Dospělý", + "teen": "Mladistvý", + "kid": "Dítě", + "path": "Cesta", + "libraryPath": "Cesta k knihovně", + "createAccount": "Vytvořit účet", + "selectMovieText1": "Vyberte správný film pro ", + "selectMovieText2": " v ", + "selectMovieSecondText": "Pokud film nenajdete, zkuste změnit název souboru", + "customMovieId": "Vlastní ID filmu", + "customIdExplication": "Pro získání ID filmu přejděte na film na TMDB a zkopírujte čísla do vyhledávacího pole", + "addThisMovie": "Přidat tento film", + "continueWatch": "Chcete pokračovat v přehrávání tam, kde jste skončili?", + "yes": "Ano", + "no": "Ne", + "back": "Zpět", + "next": "Další", + "previous": "Předchozí", + "gamesConsole": "Hry na xxconsoleNamexx", + "searchText": "Hledat film/seriál", + "offline": "Zdá se, že jste offline", + "languages": "Jazyky", + "createNewLib": "Vytvořit novou knihovnu", + "serverPort": "Port serveru", + "tmdbApiKey": "TMDB API klíč", + "igdbIdKey": "IGDB ID klíč", + "igdbSecretKey": "IGDB tajný klíč", + "saveSettings": "Uložit nastavení", + "problemsWith": "Problémy s", + "dontUnderstand": "Nerozumím", + "login": "Přihlásit se" + }, + "DA": { + "movies": "Film", + "series": "Serier", + "consoles": "Konsoller", + "tvChannels": "TV-kanaler", + "other": "Andet", + "watchNow": "Se nu", + "downloadButton": "Hent", + "allowDownloads": "Tillad downloads", + "name": "Navn", + "username": "Brugernavn", + "users": "Brugere", + "password": "Adgangskode", + "editProfile": "Rediger profil", + "profilePic": "Profilbillede", + "accountType": "Konto type", + "admin": "Administrator", + "adult": "Voksen", + "teen": "Teenager", + "kid": "Barn", + "path": "Sti", + "libraryName": "Bibliotek navn", + "libraryPath": "Bibliotek sti", + "createAccount": "Opret konto", + "selectMovieText1": "Vælg den rigtige film til ", + "selectMovieText2": " i ", + "selectMovieSecondText": "Hvis du ikke kan finde filmen, prøv at ændre filnavnet", + "customMovieId": "Tilpasset film-ID", + "customIdExplication": "For at få film-ID'et skal du gå til filmen på TMDB og kopiere tallene i søgefeltet", + "addThisMovie": "Tilføj denne film", + "continueWatch": "Vil du fortsætte med at se der, hvor du stoppede?", + "yes": "Ja", + "no": "Nej", + "back": "Tilbage", + "next": "Næste", + "previous": "Forrige", + "games console": "Spil til xxconsoleNamexx", + "searchText": "Søg efter film/serie", + "offline": "Du ser ud til at være offline", + "languages": "Sprog", + "createNewLib": "Opret nyt bibliotek", + "serverPort": "Server port", + "tmdbApiKey": "TMDB API nøgle", + "igdbIdKey": "IGDB ID nøgle", + "igdbSecretKey": "IGDB hemmelig nøgle", + "saveSettings": "Gem indstillinger", + "problemsWith": "Problemer med", + "dontUnderstand": "Forstår ikke", + "login": "Log ind" + }, "DE": { "movies": "Filme", "series": "Serien", "consoles": "Konsolen", "tvChannels": "TV-Kanäle", + "other": "Andere", "watchNow": "Jetzt ansehen", "downloadButton": "Herunterladen", "allowDownloads": "Herunterladen erlauben", @@ -589,6 +700,7 @@ "series": "Series", "consoles": "Consoles", "tvChannels": "TV Channels", + "other": "Other", "watchNow": "Watch Now", "downloadButton": "Download", "allowDownloads": "Allow Downloads", @@ -633,11 +745,62 @@ "dontUnderstand": "I don't understand", "login": "Login" }, + "EO": { + "movies": "Filmoj", + "series": "Serioj", + "consoles": "Konzoloj", + "tvChannels": "TV-kanaloj", + "other": "Aliaj", + "watchNow": "Nun spekti", + "downloadButton": "Elŝuti", + "allowDownloads": "Permesi elŝutojn", + "name": "Nomo", + "username": "Salutnomo", + "users": "Uzantoj", + "password": "Pasvorto", + "editProfile": "Redakti profilon", + "profilePic": "Profila bildo", + "accountType": "Kontotipo", + "admin": "Administranto", + "adult": "Plenkreskulo", + "teen": "Adoleskanto", + "kid": "Infano", + "path": "Vojo", + "libraryName": "Biblioteko nomo", + "libraryPath": "Biblioteko vojo", + "createAccount": "Krei konton", + "selectMovieText1": "Elektu la ĝustan filmon por ", + "selectMovieText2": " en ", + "selectMovieSecondText": "Se vi ne povas trovi la filmon, provu ŝanĝi la dosiernomon", + "customMovieId": "Personigita filmo-ID", + "customIdExplication": "Por ricevi la filmo-ID, iru al la filmo sur TMDB kaj kopiu la ciferojn en la serĉkampo", + "addThisMovie": "Aldoni ĉi tiun filmon", + "continueWatch": "Ĉu vi volas daŭrigi la spektadon tie, kie vi haltis?", + "yes": "Jes", + "no": "Ne", + "back": "Reen", + "next": "Sekva", + "previous": "Antaŭa", + "games console": "Ludoj por xxconsoleNamexx", + "searchText": "Serĉi filmon/serion", + "offline": "Vi ŝajnas esti senrete", + "languages": "Lingvoj", + "createNewLib": "Krei novan bibliotekon", + "serverPort": "Servilo pordo", + "tmdbApiKey": "TMDB API ŝlosilo", + "igdbIdKey": "IGDB ID ŝlosilo", + "igdbSecretKey": "IGDB sekreta ŝlosilo", + "saveSettings": "Konservi agordojn", + "problemsWith": "Problemoj kun", + "dontUnderstand": "Mi ne komprenas", + "login": "Ensaluti" + }, "ES": { "movies": "Películas", "series": "Series", "consoles": "Consolas", "tvChannels": "Canales de TV", + "other": "Otro", "watchNow": "Ver ahora", "downloadButton": "Descargar", "allowDownloads": "Permitir descargas", @@ -682,11 +845,62 @@ "dontUnderstand": "No entiendo", "login": "Iniciar sesión" }, + "ET": { + "movies": "Filmid", + "series": "Sari", + "consoles": "Konsoolid", + "tvChannels": "TV kanalid", + "other": "Muu", + "watchNow": "Vaata kohe", + "downloadButton": "Lae alla", + "allowDownloads": "Luba allalaadimised", + "name": "Nimi", + "username": "Kasutajanimi", + "users": "Kasutajad", + "password": "Parool", + "editProfile": "Muuda profiili", + "profilePic": "Profiilipilt", + "accountType": "Konto tüüp", + "admin": "Administraator", + "adult": "Täiskasvanu", + "teen": "Noorem", + "kid": "Laps", + "path": "Tee", + "libraryName": "Raamatukogu nimi", + "libraryPath": "Raamatukogu tee", + "createAccount": "Loo konto", + "selectMovieText1": "Vali õige film ", + "selectMovieText2": " jaoks ", + "selectMovieSecondText": "Kui sa ei leia filmi, proovi muuta failinime", + "customMovieId": "Kohandatud filmi ID", + "customIdExplication": "Filmivõtme saamiseks mine filmile TMDB-s ja kopeeri numbrit otsingusse", + "addThisMovie": "Lisa see film", + "continueWatch": "Kas soovid jätkata vaatamist sealt, kus jäid?", + "yes": "Jah", + "no": "Ei", + "back": "Tagasi", + "next": "Järgmine", + "previous": "Eelmine", + "games console": "Mängud xxconsoleNamexx jaoks", + "searchText": "Otsi filme/sarju", + "offline": "Paistab, et sa oled võrgust väljas", + "languages": "Keeled", + "createNewLib": "Loo uus raamatukogu", + "serverPort": "Serveri port", + "tmdbApiKey": "TMDB API võti", + "igdbIdKey": "IGDB ID võti", + "igdbSecretKey": "IGDB salajane võti", + "saveSettings": "Salvesta seaded", + "problemsWith": "Probleemid", + "dontUnderstand": "Ma ei saa aru", + "login": "Logi sisse" + }, "EU": { "movies": "Filmeak", "series": "Serieak", "consoles": "Konsolak", "tvChannels": "TV Kanalak", + "other": "Beste bat", "watchNow": "Orain ikusi", "downloadButton": "Deskargatu", "allowDownloads": "Deskargak onartu", @@ -731,11 +945,62 @@ "dontUnderstand": "Ez dut ulertzen", "login": "Saioa hasi" }, + "FI": { + "movies": "Elokuvat", + "series": "Sarjat", + "consoles": "Konsolit", + "tvChannels": "TV-kanavat", + "other": "Muut", + "watchNow": "Katso nyt", + "downloadButton": "Lataa", + "allowDownloads": "Salli lataukset", + "name": "Nimi", + "username": "Käyttäjätunnus", + "users": "Käyttäjät", + "password": "Salasana", + "editProfile": "Muokkaa profiilia", + "profilePic": "Profiilikuva", + "accountType": "Tilin tyyppi", + "admin": "Ylläpitäjä", + "adult": "Aikuinen", + "teen": "Nuori", + "kid": "Lapsi", + "path": "Polku", + "libraryName": "Kirjaston nimi", + "libraryPath": "Kirjaston polku", + "createAccount": "Luo tili", + "selectMovieText1": "Valitse oikea elokuva ", + "selectMovieText2": " varten ", + "selectMovieSecondText": "Jos et löydä elokuvaa, kokeile vaihtaa tiedoston nimeä", + "customMovieId": "Mukautettu elokuvan tunniste", + "customIdExplication": "Saadaksesi elokuvan tunnuksen, mene elokuvaan TMDB:ssä ja kopioi numero hakukenttään", + "addThisMovie": "Lisää tämä elokuva", + "continueWatch": "Haluatko jatkaa katselua siitä, mihin jäit?", + "yes": "Kyllä", + "no": "Ei", + "back": "Takaisin", + "next": "Seuraava", + "previous": "Edellinen", + "games console": "Pelit xxconsoleNamexx", + "searchText": "Etsi elokuvia/sarjoja", + "offline": "Näytät olevan offline-tilassa", + "languages": "Kielet", + "createNewLib": "Luo uusi kirjasto", + "serverPort": "Palvelimen portti", + "tmdbApiKey": "TMDB API -avain", + "igdbIdKey": "IGDB ID -avain", + "igdbSecretKey": "IGDB salainen avain", + "saveSettings": "Tallenna asetukset", + "problemsWith": "Ongelmat", + "dontUnderstand": "En ymmärrä", + "login": "Kirjaudu sisään" + }, "FR": { "movies": "Films", "series": "Series", "consoles": "Consoles", "tvChannels": "Chaines TV", + "other": "Autre", "watchNow": "Regarder maintenant", "downloadButton": "Télécharger", "allowDownloads": "Autoriser les téléchargements", @@ -780,11 +1045,62 @@ "dontUnderstand": "Je ne comprends pas", "login": "Connexion" }, + "HI": { + "movies": "फिल्में", + "series": "सीरीज", + "consoles": "कंसोल", + "tvChannels": "टीवी चैनल", + "other": "अन्य", + "watchNow": "अभी देखें", + "downloadButton": "डाउनलोड करें", + "allowDownloads": "डाउनलोड की अनुमति दें", + "name": "नाम", + "username": "यूजरनेम", + "users": "उपयोगकर्ता", + "password": "पासवर्ड", + "editProfile": "प्रोफ़ाइल संपादित करें", + "profilePic": "प्रोफ़ाइल चित्र", + "accountType": "खाते का प्रकार", + "admin": "व्यवस्थापक", + "adult": "वयस्क", + "teen": "टीन", + "kid": "बच्चा", + "path": "पथ", + "libraryName": "पुस्तकालय का नाम", + "libraryPath": "पुस्तकालय की पथ", + "createAccount": "खाता बनाएं", + "selectMovieText1": "को लिए सही फिल्म चुनें ", + "selectMovieText2": " में ", + "selectMovieSecondText": "यदि आपको फिल्म नहीं मिल रही है, तो फ़ाइल का नाम बदलने का प्रयास करें", + "customMovieId": "कस्टम फ़िल्म आईडी", + "customIdExplication": "फ़िल्म आईडी प्राप्त करने के लिए, टीएमडीबी पर फ़िल्म पर जाएं और खोजबार में अंक की प्रतिलिपि निकालें", + "addThisMovie": "इस फ़िल्म जोड़ें", + "continueWatch": "आपने जहाँ रुक गए हैं, वहाँ से देखना जारी रखना है?", + "yes": "हाँ", + "no": "नहीं", + "back": "वापस", + "next": "अगला", + "previous": "पिछला", + "gamesConsole": "xxconsoleNamexx की खेल", + "searchText": "एक फ़िल्म/सीरीज खोजें", + "offline": "आप ऑफ़लाइन लगते हैं", + "languages": "भाषाएँ", + "createNewLib": "नया पुस्तकालय बनाएँ", + "serverPort": "सर्वर पोर्ट", + "tmdbApiKey": "TMDB API कुंजी", + "igdbIdKey": "IGDB ID कुंजी", + "igdbSecretKey": "IGDB गुप्त कुंजी", + "saveSettings": "सेटिंग्स सहेजें", + "problemsWith": "की समस्याएँ", + "dontUnderstand": "मैं समझ नहीं पाता", + "login": "लॉग इन करें" + }, "HR": { "movies": "Filmovi", "series": "Serije", "consoles": "Konzole", "tvChannels": "TV kanali", + "other": "Ostalo", "watchNow": "Gledaj sada", "downloadButton": "Preuzmi", "allowDownloads": "Dopusti preuzimanje", @@ -834,6 +1150,7 @@ "series": "Սերիաներ", "consoles": "Համակարգիչներ", "tvChannels": "Տեսահոլովակներ", + "other": "Այլ", "watchNow": "Տեսնել հիմա", "downloadButton": "Ներբեռնել", "allowDownloads": "Թույլատրել ներբեռնումը", @@ -883,6 +1200,7 @@ "series": "Serie TV", "consoles": "Console", "tvChannels": "Canali TV", + "other": "Altro", "watchNow": "Guarda ora", "downloadButton": "Scarica", "allowDownloads": "Consenti download", @@ -926,5 +1244,155 @@ "problemsWith": "Problemi con", "dontUnderstand": "Non capisco", "login": "Accedi" + }, + "NL": { + "movies": "Films", + "series": "Series", + "consoles": "Consoles", + "tvChannels": "TV-kanalen", + "other": "Overig", + "watchNow": "Nu bekijken", + "downloadButton": "Downloaden", + "allowDownloads": "Downloads toestaan", + "name": "Naam", + "username": "Gebruikersnaam", + "users": "Gebruikers", + "password": "Wachtwoord", + "editProfile": "Profiel bewerken", + "profilePic": "Profielfoto", + "accountType": "Accounttype", + "admin": "Beheerder", + "adult": "Volwassene", + "teen": "Tiener", + "kid": "Kind", + "path": "Pad", + "libraryName": "Naam bibliotheek", + "libraryPath": "Pad naar bibliotheek", + "createAccount": "Account aanmaken", + "selectMovieText1": "Selecteer de juiste film voor ", + "selectMovieText2": " in ", + "selectMovieSecondText": "Als je de film niet kunt vinden, probeer dan de naam van het bestand te veranderen", + "customMovieId": "Aangepaste film-ID", + "customIdExplication": "Om het film-ID te krijgen, ga je naar de film op TMDB en kopieer je de cijfers in het zoekveld", + "addThisMovie": "Voeg deze film toe", + "continueWatch": "Wil je verder kijken waar je gebleven was?", + "yes": "Ja", + "no": "Nee", + "back": "Terug", + "next": "Volgende", + "previous": "Vorige", + "games console": "Spellen voor xxconsoleNamexx", + "searchText": "Zoek naar film/serie", + "offline": "Je lijkt offline te zijn", + "languages": "Talen", + "createNewLib": "Maak een nieuwe bibliotheek aan", + "serverPort": "Serverpoort", + "tmdbApiKey": "TMDB API-sleutel", + "igdbIdKey": "IGDB ID-sleutel", + "igdbSecretKey": "IGDB geheime sleutel", + "saveSettings": "Instellingen opslaan", + "problemsWith": "Problemen met", + "dontUnderstand": "Ik begrijp het niet", + "login": "Aanmelden" + }, + "NY": { + "movies": "Zabwino", + "series": "Zapatali", + "consoles": "Zambiri zakudya zonse", + "tvChannels": "Zambiri zatrikhulu", + "other": "Zambiri zonse", + "watchNow": "Sangalalani", + "downloadButton": "Landirani", + "allowDownloads": "Zikwanirani zapatali zonse", + "name": "Dzina", + "username": "Dzina la mtumiki", + "users": "Watumiki", + "password": "Manena a akaunti", + "editProfile": "Sanulani magawo", + "profilePic": "Chitadzo cha magawo", + "accountType": "Mtengo wa akaunti", + "admin": "Mtumiki wa kutsatira", + "adult": "Mtumiki wa kukula", + "teen": "Mtumiki wa kulukuta", + "kid": "Mtumiki wa zaka", + "path": "Mwayi", + "libraryName": "Dzina la chitolero", + "libraryPath": "Mwayi wa chitolero", + "createAccount": "Pangani akaunti", + "selectMovieText1": "Tanthauza zabwino lonse kwa ", + "selectMovieText2": " pa ", + "selectMovieSecondText": "Koma mukutheka kuti sanapitse zabwino, tsopano sanawona dzina la fayiro", + "customMovieId": "ID yabwino yatsopano", + "customIdExplication": "Kuti sanapanga ID ya zabwino, tembenuka pa zabwino pa TMDB, patsopano sanakoperera nambiri pa chinso chonse cha kuchita", + "addThisMovie": "Onanitsa zabwino izi", + "continueWatch": "Mukufuna kusangalalani kuona uliwo?", + "yes": "Eeh", + "no": "Sindikufuna", + "back": "Pepala", + "next": "Pachina", + "previous": "Paposoneka", + "gamesConsole": "Zambiri zakudya za xxconsoleNamexx", + "searchText": "Chita zabwino/zapatali", + "offline": "Mwakufuna kuti mukudya pano", + "languages": "Zilimi", + "createNewLib": "Pangani chitolero chambiri", + "serverPort": "Mtengo wa kufotokoza", + "tmdbApiKey": "TMDB-API-chinsinsi", + "igdbIdKey": "IGDB-ID-chinsinsi", + "igdbSecretKey": "IGDB-chinsinsi chachikulu", + "saveSettings": "Tenga zina", + "problemsWith": "Makina pa", + "dontUnderstand": "Sakufuna kufunika", + "login": "Kulogin" + }, + "ZH": { + "movies": "电影", + "series": "系列", + "consoles": "控制台", + "tvChannels": "电视频道", + "other": "其他", + "watchNow": "现在观看", + "downloadButton": "下载", + "allowDownloads": "允许下载", + "name": "名称", + "username": "用户名", + "users": "用户", + "password": "密码", + "editProfile": "编辑资料", + "profilePic": "个人资料图片", + "accountType": "帐户类型", + "admin": "管理员", + "adult": "成人", + "teen": "青少年", + "kid": "孩子", + "path": "路径", + "libraryName": "图书馆名称", + "libraryPath": "图书馆路径", + "createAccount": "创建帐户", + "selectMovieText1": "为 ", + "selectMovieText2": " 选择正确的电影 ", + "selectMovieSecondText": "如果找不到电影,请尝试更改文件名", + "customMovieId": "自定义电影 ID", + "customIdExplication": "要获取电影 ID,请前往 TMDB 上的电影,并将数字复制到搜索栏中", + "addThisMovie": "添加此电影", + "continueWatch": "你想从你停止的地方继续观看吗?", + "yes": "是", + "no": "否", + "back": "返回", + "next": "下一步", + "previous": "上一步", + "games console": "xxconsoleNamexx 游戏", + "searchText": "搜索电影/电视剧", + "offline": "你似乎处于离线状态", + "languages": "语言", + "createNewLib": "创建新图书馆", + "serverPort": "服务器端口", + "tmdbApiKey": "TMDB API 密钥", + "igdbIdKey": "IGDB ID 密钥", + "igdbSecretKey": "IGDB 秘密密钥", + "saveSettings": "保存设置", + "problemsWith": "问题", + "dontUnderstand": "我不明白", + "login": "登录" } } \ No newline at end of file diff --git a/templates/allFilms.html b/templates/allFilms.html index b951883..32f9f00 100644 --- a/templates/allFilms.html +++ b/templates/allFilms.html @@ -1,8 +1,34 @@ -{% extends 'header.html' %}{% block title %}Movies Library{% endblock %} {% block content %} {% if conditionIfOne %} +{% extends 'header.html' %}{% block title %}{{ library }}{% endblock %} {% block content %} {% if conditionIfOne %}

{{ errorMessage }}

-{% else %} {% include 'popup.html' %} +{% else %} {% if g.currentUser["accountType"] == "Admin" %} + {% endif %} + +
+
+ +
+
+
+
+
+
+ + + + +{% include 'popup.html' %}

diff --git a/templates/allSeries.html b/templates/allSeries.html index e21742d..5ad0559 100644 --- a/templates/allSeries.html +++ b/templates/allSeries.html @@ -2,7 +2,32 @@

{{ errorMessage }}

-{% else %} {% include 'popup.html' %} +{% else %} {% if g.currentUser["accountType"] == "Admin" %} + {% endif %} + +
+
+ +
+
+
+
+
+
+ + + +{% include 'popup.html' %}

diff --git a/templates/consoles.html b/templates/consoles.html index a07bea0..cde30fb 100644 --- a/templates/consoles.html +++ b/templates/consoles.html @@ -1,4 +1,8 @@ -{% extends 'header.html' %}{% block title %}Consoles{% endblock %}{% block content %} {% include 'popup.html' %} +{% extends 'header.html' %}{% block title %}Consoles{% endblock %}{% block content %} {% include 'popup.html' %} {% if g.currentUser["accountType"] == "Admin" %} + {% endif %}

diff --git a/templates/film.html b/templates/film.html index 364a8bd..f7149c1 100644 --- a/templates/film.html +++ b/templates/film.html @@ -4,13 +4,20 @@

{{ g.language["continueWatch"] }}

{{ g.language["yes"] }} {{ g.language["no"] }}
-
- - +
+
+
+ + {% for audioSource in allAudioSources %} + {% endfor %} + +
+
+
+ {% for audioSource in allAudioSources %} +
+ {% endfor %} +
+ {% endblock %} \ No newline at end of file diff --git a/templates/games.html b/templates/games.html index 41ed829..08b5f4b 100644 --- a/templates/games.html +++ b/templates/games.html @@ -2,7 +2,7 @@

{{ errorMessage }}

-{% else %} {% include 'popup.html' %} +{% else %}{% include 'popup.html' %}

{{ g.language["back"] }}

diff --git a/templates/header.html b/templates/header.html index 6c6a562..306e9a5 100644 --- a/templates/header.html +++ b/templates/header.html @@ -6,7 +6,7 @@ - + @@ -30,7 +30,8 @@ - {% block head %}{% endblock %} + + {% block head %}{% endblock %} {% block title %}{% endblock %} | Chocolate diff --git a/templates/login.html b/templates/login.html index 9f461ce..f034c56 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,7 +1,7 @@ {% extends 'header.html' %}{% block title %}Login{% endblock %} {% block content %}
{% for user in allUsers %} -
+
{{ user.name }}

{{ user.name }}

diff --git a/templates/other.html b/templates/other.html new file mode 100644 index 0000000..8be4129 --- /dev/null +++ b/templates/other.html @@ -0,0 +1,29 @@ +{% extends 'header.html' %}{% block title %}{{ library }}{% endblock %} {% block content %} + + +
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/otherVideo.html b/templates/otherVideo.html new file mode 100644 index 0000000..4392620 --- /dev/null +++ b/templates/otherVideo.html @@ -0,0 +1,16 @@ +{% extends 'header.html' %}{% block title %}{{ title }}{% endblock %}{% block content %} + +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/templates/profil.html b/templates/profil.html index b53e142..962a551 100644 --- a/templates/profil.html +++ b/templates/profil.html @@ -3,8 +3,8 @@ {{ user.name }} - - + {% if user.accountType != "Kid" %} + {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/serie.html b/templates/serie.html index cd8cb09..b20e43e 100644 --- a/templates/serie.html +++ b/templates/serie.html @@ -4,13 +4,15 @@

{{ g.language["continueWatch"] }}

{{ g.language["yes"] }} {{ g.language["no"] }}
-
- +
+
+
+ + {% for caption in allCaptions %} + {% endfor %} + +
+
diff --git a/templates/settings.html b/templates/settings.html index 2c8c65f..93c2d68 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -27,6 +27,12 @@
{{ g.language["tvChannels"] }} + +
@@ -86,6 +92,12 @@

{{ g.language["users"] }}:

+
+ + +
diff --git a/templates/tv.html b/templates/tv.html index 2a8d213..2f7c682 100644 --- a/templates/tv.html +++ b/templates/tv.html @@ -1,6 +1,7 @@ -{% extends 'header.html' %}{% block title %}TV{% endblock %}{% block content %} +{% extends 'header.html' %}{% block title %}{{ channelName }}TV{% endblock %}{% block content %} +
+
-