This commit is contained in:
darkeye 2025-07-08 16:50:48 +02:00
commit 3b6b7abb38
20 changed files with 837 additions and 0 deletions

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "dashboard",
"version": "1.0.0",
"description": "Dashboard for LS Services",
"private": true,
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"author": "darkeye",
"license": "ISC",
"devDependencies": {
"css-loader": "^6.10.0",
"html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"mini-css-extract-plugin": "^2.8.1",
"postcss-loader": "^8.1.1",
"style-loader": "^3.3.4",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-stream": "^7.0.0"
}
}

73
src/css/main.css Normal file
View File

@ -0,0 +1,73 @@
* {
box-sizing: border-box;
font-family: "Open Sans";
}
a {
text-decoration: none;
color: inherit;
}
html {
position: relative;
}
body {
position: relative;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-content: stretch;
justify-content: space-between;
min-width: 100vw;
min-height: 100vh;
background-color: var(--background);
color: var(--on-background);
}
header {
background-color: var(--background-level-5);
color: var(--on-surface);
}
main {
flex: 1;
overflow-y: auto;
color: var(--on-surface);
}
footer {
background-color: var(--background-level-5);
color: var(--on-surface);
}
.top-bar {
position: sticky;
top: 0;
overflow: hidden;
margin: 150px 30px 15px 30px;
color: white;
display: flex;
justify-content: space-between;
}
.main-content{
display: flex;
}
.side-bar{
width: 25vw;
background-color: blue;
}
.video-overview{
flex-grow: 1
}
#ls-player{
border: solid thin red;
width: 100%
}

34
src/html/index.html Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mediaplayer</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app-container">
<div id="full-page-background"></div>
<div id="top-bar" class="top-bar">
<div>Continue</div>
<div>search</div>
</div>
<div id="main-content" class="main-content">
<div id="side-bar" class="side-bar">left-bar</div>
<div id="search-results" class="search-results"></div>
<div id="video-overview" class="video-overview">
<div id="video-title">Test Titel</div>
<div id="video-tags"></div>
<div id="video-description">Test Titel</div>
<div id="video-player-container">
<video id="ls-player"></video>
</div>
</div>
</div>
</div>
<input type="button" id="startAppButton" value="Start" />
<video id="video" width="320" height="240" controls></video>
</body>
</html>

27
src/js/MediaPlayer.js Normal file
View File

@ -0,0 +1,27 @@
import MediaLibraryDescriptorLoader from "./loader/MediaLibraryDescriptorLoader.js";
export default class Mediaplayer{
#rootFolderHandler = null;
#libraryDescriptor = null;
async start(){
try {
this.#rootFolderHandler = await showDirectoryPicker({"id": "mediaplayer_amin", "mode": "readwrite"});
this.#libraryDescriptor = await MediaLibraryDescriptorLoader.loadDescriptor(this.#rootFolderHandler);
this.#initPlayer();
} catch (error) {
console.log("Unable to start media player!");
console.error(error);
}
}
#initPlayer(){
//change ui to msg dialog "loading library ..."
this.#loadPlaylists();
//init ui
}
#loadPlaylists(){
}
}

View File

@ -0,0 +1,20 @@
export default class FileDescriptor {
/**
* @type {string}
*/
path = "";
/**
* @type {FileSystemFileHandle}
*/
handle = "";
/**
* @param {string} path
* @param {FileSystemFileHandle} handle
*/
constructor(path, handle){
this.path = path;
this.handle = handle;
}
}

View File

@ -0,0 +1,23 @@
import MediaProfile from "../profile/MediaProfile.js";
export default class MediaLibraryDescriptor {
/**
* @type {string}
*/
name = "";
/**
* @type {string}
*/
description = "";
/**
* @type {string}
*/
rootPath = "./";
/**
* @type {MediaProfile[]}
*/
profiles = [];
}

View File

@ -0,0 +1,31 @@
export default class MediaPlaylistDescriptor {
/**
* @type {string}
*/
name = "";
/**
* @type {string}
*/
description = "";
/**
* @type {string | null}
*/
cover = null;
/**
* @type {string}
*/
type = PlaylistType.SERIES;
/**
* @type {string[]}
*/
tags = [];
/**
* @type {Track[]}
*/
tracks = [];
}

38
src/js/index.js Normal file
View File

@ -0,0 +1,38 @@
import css from "../css/main.css";
import MediaProfile from "./profile/MediaProfile.js";
import FSUtil from "./util/FSUtil.js";
async function load(){
const dir = await showDirectoryPicker({"id": "mediaplayer_amin", "mode": "readwrite"});
await walkFileTree(dir);
}
async function walkFileTree(currentDir){
for await (const value of currentDir.values()) {
if(value.kind == 'directory'){
await walkFileTree(value);
} else if(value.kind == 'file' && value.name == 'playlist.json'){
await getVideo(currentDir, value);
}
}
}
async function getVideo(dir, playlistFile){
const file = await playlistFile.getFile();
console.log("Playlist file:", file);
const content = await file.text();
const playlist = JSON.parse(content);
console.log(playlist);
const videoPath = playlist.tracks[0].relativePath;
console.log(videoPath);
const videoHandle = await FSUtil.getFileHandle(dir, videoPath);
console.log(videoHandle);
playVideo(await videoHandle.getFile());
}
function playVideo(file){
document.getElementById("video").src = URL.createObjectURL(file);
}
document.getElementById("startAppButton").addEventListener("click", load);

View File

@ -0,0 +1,46 @@
import MediaLibraryDescriptor from "../descriptor/MediaLibraryDescriptor.js";
import MediaProfile from "../profile/MediaProfile.js";
export default class MediaLibraryDescriptorLoader {
/**
* @param {FileSystemDirectoryHandle} rootFolderHandler
* @returns {Promise<MediaLibraryDescriptor>}
*/
static async loadDescriptor(rootFolderHandler) {
const mediaLibrary = new MediaLibraryDescriptor();
try {
const mediaDescriptorHandler = await rootFolderHandler.getFileHandle("media_library.json", { create: true });
const descriptorFile = await mediaDescriptorHandler.getFile();
const descriptionFileContent = await descriptorFile.text();
if (descriptionFileContent == null || descriptionFileContent.length < 10) {
return mediaLibrary;
}
const mediaDescriptorObj = JSON.parse(descriptionFileContent);
if (mediaDescriptorObj != null && mediaDescriptorObj.name != null) {
mediaLibrary.name = mediaDescriptorObj.name;
}
if (mediaDescriptorObj != null && mediaDescriptorObj.description != null) {
mediaLibrary.description = mediaDescriptorObj.description;
}
if (mediaDescriptorObj != null && mediaDescriptorObj.root != null) {
mediaLibrary.root = mediaDescriptorObj.root;
}
if (mediaDescriptorObj != null && mediaDescriptorObj.profiles != null) {
mediaDescriptorObj.profiles.forEach(d => {
mediaLibrary.profiles.push(MediaProfile.fromJson(d));
});
}
} catch (error) {
return mediaLibrary;
}
return mediaLibrary;
}
}

View File

@ -0,0 +1,46 @@
import MediaLibraryDescriptor from "../descriptor/MediaLibraryDescriptor.js";
import MediaProfile from "../profile/MediaProfile.js";
export default class MediaPlaylistDescriptorLoader {
/**
* @param {FileSystemDirectoryHandle} rootFolderHandler
* @returns {Promise<MediaLibraryDescriptor>}
*/
static async loadDescriptors(rootFolderHandler) {
const mediaLibrary = new MediaLibraryDescriptor();
try {
const mediaDescriptorHandler = await rootFolderHandler.getFileHandle("media_library.json", { create: true });
const descriptorFile = await mediaDescriptorHandler.getFile();
const descriptionFileContent = await descriptorFile.text();
if (descriptionFileContent == null || descriptionFileContent.length < 10) {
return mediaLibrary;
}
const mediaDescriptorObj = JSON.parse(descriptionFileContent);
if (mediaDescriptorObj != null && mediaDescriptorObj.name != null) {
mediaLibrary.name = mediaDescriptorObj.name;
}
if (mediaDescriptorObj != null && mediaDescriptorObj.description != null) {
mediaLibrary.description = mediaDescriptorObj.description;
}
if (mediaDescriptorObj != null && mediaDescriptorObj.root != null) {
mediaLibrary.root = mediaDescriptorObj.root;
}
if (mediaDescriptorObj != null && mediaDescriptorObj.profiles != null) {
mediaDescriptorObj.profiles.forEach(d => {
mediaLibrary.profiles.push(MediaProfile.fromJson(d));
});
}
} catch (error) {
return mediaLibrary;
}
return mediaLibrary;
}
}

View File

@ -0,0 +1,39 @@
import PlaylistType from "./PlaylistType.js";
import Track from "./Track.js";
export default class Playlist {
/**
* @type {string}
*/
path = "";
/**
* @type {string}
*/
name = "";
/**
* @type {string}
*/
description = "";
/**
* @type {string | null}
*/
cover = null;
/**
* @type {string}
*/
type = PlaylistType.SERIES;
/**
* @type {string[]}
*/
tags = [];
/**
* @type {Track[]}
*/
tracks = [];
}

View File

@ -0,0 +1,16 @@
export default class PlaylistType {
/**
* @type {string}
*/
static SERIES = "SERIES";
/**
* @type {string}
*/
static MOVIE = "MOVIE";
/**
* @type {string}
*/
static AUDIO = "AUDIO";
}

28
src/js/playlist/Track.js Normal file
View File

@ -0,0 +1,28 @@
import TrackMark from "./TrackMark.js";
export default class Track {
/**
* @type {string | null}
*/
group = null;
/**
* @type {string}
*/
title = "";
/**
* @type {string}
*/
format = "";
/**
* @type {string}
*/
path = "";
/**
* @type {TrackMark[]}
*/
marks = [];
}

View File

@ -0,0 +1,6 @@
export default class TrackMark {
start = -1;
end = -1;
type = "UNKNOWN";
name = "";
}

View File

@ -0,0 +1,55 @@
import MediaProfileRole from "./MediaProfileRole.js";
import MediaProfileSettings from "./MediaProfileSettings.js";
import MediaProfileViewSettings from "./MediaProfileViewSettings.js";
export default class MediaProfile{
/**
* @type {string}
*/
name = "";
/**
* @type {string}
*/
role = MediaProfileRole.ADMIN;
/**
* @type {MediaProfileSettings}
*/
settings = new MediaProfileSettings();
/**
* @type {MediaProfileViewSettings}
*/
playSettings = new MediaProfileViewSettings();
/**
* @param {object} jsonObj
* @returns {MediaProfile}
*/
static fromJson(jsonObj) {
const profile = new MediaProfile();
if (jsonObj == null) {
return profile;
}
if (jsonObj.name != null) {
profile.name = jsonObj.name;
}
if (jsonObj.role != null) {
profile.role = jsonObj.role;
}
if (jsonObj.settings != null && typeof jsonObj.settings === "object") {
profile.settings = MediaProfileSettings.fromSettingsJson(jsonObj.settings);
}
if (jsonObj.playSettings != null && typeof jsonObj.playSettings === "object") {
profile.playSettings = MediaProfileViewSettings.fromSettingsJson(jsonObj.playSettings);
}
return profile;
}
}

View File

@ -0,0 +1,11 @@
export default class MediaProfileRole {
/**
* @type {string}
*/
static ADMIN = "ADMIN";
/**
* @type {string}
*/
static USER = "USER";
}

View File

@ -0,0 +1,51 @@
export default class MediaProfileSettings {
/**
* @type {boolean}
*/
skipIntro = true;
/**
* @type {boolean}
*/
skipOutro = true;
/**
* @type {boolean}
*/
skipRecall = true;
/**
* @type {boolean}
*/
skipPreview = true;
/**
* @param {object} jsonObj
* @returns {MediaProfileSettings}
*/
static fromSettingsJson(jsonObj) {
const settings = new MediaProfileSettings();
if (jsonObj == null) {
return settings;
}
if (jsonObj.skipIntro != null) {
settings.skipIntro = jsonObj.skipIntro;
}
if (jsonObj.skipOutro != null) {
settings.skipOutro = jsonObj.skipOutro;
}
if (jsonObj.skipRecall != null) {
settings.skipRecall = jsonObj.skipRecall;
}
if (jsonObj.skipPreview != null) {
settings.skipPreview = jsonObj.skipPreview;
}
return settings;
}
}

View File

@ -0,0 +1,51 @@
export default class MediaProfileViewSettings {
/**
* @type {boolean}
*/
hasOpenPlaylist = false;
/**
* @type {string}
*/
playlistPath = null;
/**
* @type {number}
*/
playlistTrackNumber = 0;
/**
* @type {number}
*/
trackPosition = 0;
/**
* @param {object} jsonObj
* @returns {MediaProfileViewSettings}
*/
static fromSettingsJson(jsonObj) {
const settings = new MediaProfileViewSettings();
if (jsonObj == null) {
return settings;
}
if (jsonObj.hasOpenPlaylist != null) {
settings.hasOpenPlaylist = jsonObj.hasOpenPlaylist;
}
if (jsonObj.playlistPath != null) {
settings.playlistPath = jsonObj.playlistPath;
}
if (jsonObj.playlistTrackNumber != null) {
settings.playlistTrackNumber = jsonObj.playlistTrackNumber;
}
if (jsonObj.trackPosition != null) {
settings.trackPosition = jsonObj.trackPosition;
}
return settings;
}
}

159
src/js/util/FSUtil.js Normal file
View File

@ -0,0 +1,159 @@
import FileDescriptor from "../descriptor/FileDescriptor.js";
export default class FSUtil {
/**
* @param {FileSystemDirectoryHandle} rootDir
* @param {string} path
* @returns {Promise<string | null>}
*/
static async getFileContent(rootDir, path) {
const fileHandle = await FSUtil.getFileHandle(rootDir, FSUtil.fixPath(path));
return await FSUtil.getFileHandleFileContent(fileHandle);
}
/**
* @param {FileSystemFileHandle} handle
* @returns {Promise<string>}
*/
static async getFileHandleFileContent(handle) {
if (handle == null) {
return null;
}
const file = await handle.getFile();
return await file.text();
}
/**
* @param {FileSystemDirectoryHandle} rootDir
* @param {string} path
* @returns {Promise<FileSystemFileHandle | null>}
*/
static async getFileHandle(rootDir, path) {
let resultDir = rootDir;
let outputFile = null;
const pathes = FSUtil.fixPath(path).split("/");
let nextDirName = null;
while (nextDirName = pathes.shift()) {
let nextDir = null;
for await (const value of resultDir.values()) {
if (value.kind == 'directory' && value.name == nextDirName) {
nextDir = value;
} else if (value.kind == 'file' && value.name == nextDirName) {
outputFile = value;
}
}
if (nextDir != null) {
resultDir = nextDir;
}
}
return outputFile;
}
/**
* @param {FileSystemDirectoryHandle} rootDir
* @param {string} path
* @returns {Promise<FileSystemDirectoryHandle | null>}
*/
static async getDirectoryHandle(curDir, path) {
let resultDir = curDir;
const pathes = FSUtil.fixPath(path).split("/");
let nextDirName = null;
while (nextDirName = pathes.shift()) {
let nextDir = null;
for await (const value of resultDir.values()) {
if (value.kind == 'directory' && value.name == nextDirName) {
nextDir = value;
}
}
if (nextDir != null) {
resultDir = nextDir;
}
}
return resultDir;
}
/**
* @param {string} path
* @returns {string}
*/
static fixPath(path) {
let fixedPath = path.replaceAll("/./", "/"); //remove same folder subpathes
fixedPath = fixedPath.replaceAll("//", "/"); //remove double slashes
if (fixedPath.startsWith("/")) {
fixedPath = fixedPath.substring(1);
}
const pathParts = fixedPath.split("/");
for (let i = pathParts.length - 1; i > 0; i--) {
if (pathParts[i] == "..") {
pathParts.splice(i - 1, 2);
i--;
}
}
return pathParts.join("/");
}
/**
* @param {FileSystemDirectoryHandle} dir
* @param {string} fileName
* @param {boolean} includeSubDirs
* @returns {Promise<FileDescriptor>}
*/
static async findFile(dir, fileName, includeSubDirs = true) {
const files = await FSUtil.#findFilesInTree(dir, fileName, includeSubDirs, true);
return files.length > 0 ? files[0] : null;
}
/**
* @param {FileSystemDirectoryHandle} dir
* @param {string} fileName
* @param {boolean} includeSubDirs
* @returns {Promise<FileDescriptor[]>}
*/
static async findFiles(dir, fileName, includeSubDirs = true) {
return FSUtil.#findFilesInTree(dir, fileName, includeSubDirs, false);;
}
/**
* @param {FileSystemDirectoryHandle} dir
* @param {string} fileName
* @param {boolean} includeSubDirs
* @param {boolean} stopOnFirst
* @returns {Promise<FileDescriptor[]>}
*/
static async #findFilesInTree(dir, fileName, includeSubDirs = true, stopOnFirst = false, path = "") {
const files = [];
const subdirs = [];
for await (const child of dir.values()) {
if (child.kind == 'directory' && includeSubDirs) {
subdirs.push(child);
} else if (child.kind == 'file' && child.name == fileName) {
files.push(new FileDescriptor(path, child));
if(stopOnFirst){
return files;
}
}
}
for (let i = 0; i < subdirs.length; i++) {
files.push(...(await FSUtil.#findFilesInTree(subdirs[i], fileName, includeSubDirs, stopOnFirst, path + "/" + subdirs[i].name)));
if(stopOnFirst && files.length > 0){
break;
}
}
return files;
}
}

59
webpack.config.js Normal file
View File

@ -0,0 +1,59 @@
import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: './src/js/index.js',
mode: 'development',
devtool: 'inline-source-map',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'target'),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Mediaplayer',
template: './src/html/index.html'
}),
new MiniCssExtractPlugin(),
],
module: {
rules: [
{
test: /\.css$/i,
oneOf: [
{
assert: { type: "css" },
loader: "css-loader",
options: {
exportType: "css-style-sheet",
}
},
{
assert: { type: "text" },
loader: "css-loader",
options: {
exportType: "string",
}
},
{
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"]
},
]
},
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
type: "asset",
},
{
test: /\.html$/i,
loader: "html-loader",
},
],
},
};