initial commit
This commit is contained in:
commit
f569d26744
5
composer.json
Normal file
5
composer.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"require": {
|
||||
"php-ffmpeg/php-ffmpeg": "^1.2"
|
||||
}
|
||||
}
|
31
edl.php
Normal file
31
edl.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
$db = new SQLite3('netv-mam.sqlite');
|
||||
|
||||
// add edl to media item
|
||||
if (isset($_GET['id']) && $_GET['id'] !== "") {
|
||||
error_log(print_r($_REQUEST, true));
|
||||
$id = intval($_GET['id']);
|
||||
$query = $db->prepare("SELECT * FROM media WHERE id = :id");
|
||||
$query->bindValue(':id', $id);
|
||||
$result = $query->execute();
|
||||
|
||||
if ($result) {
|
||||
// record exists
|
||||
$update_query = $db->prepare("INSERT INTO media_edls (media_id, edl_name, edl_contents) VALUES (:media_id, :edl_name, :edl_contents)");
|
||||
$update_query->bindValue(':media_id', $_REQUEST['id']);
|
||||
$update_query->bindValue(':edl_name', $_REQUEST['edl_name']);
|
||||
$update_query->bindValue(':edl_contents', $_REQUEST['edl_contents']);
|
||||
$update_result = $update_query->execute();
|
||||
if ($update_result) {
|
||||
$query = $db->prepare("SELECT * FROM media m JOIN media_edls em ON m.id = me.media_id WHERE m.id = :id");
|
||||
$query->bindValue(':id', $id);
|
||||
$final_result = $query->execute();
|
||||
if ($final_result) {
|
||||
echo(json_encode($final_result->fetchArray(SQLITE3_ASSOC)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
226
index.php
Normal file
226
index.php
Normal file
@ -0,0 +1,226 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>NETV MAM</title>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#upload_target {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 25vw;
|
||||
height: 10vh;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.readyToDrop {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
#top_pane {
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
width: 100vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
#info_pane {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#cue_pane {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#live_pane {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#calendar_pane {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
width: 100vw;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
#tab_handles {
|
||||
display: inline-flex;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
width: 100vw;
|
||||
height: 1vh;
|
||||
}
|
||||
|
||||
.tabHandle {
|
||||
border: 1px solid black;
|
||||
padding: 2vh 1vw;
|
||||
}
|
||||
|
||||
.activeTabHandle {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: none;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.activeTab {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#cue_deck, #live_deck {
|
||||
width: 320px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>NETV MAM</h1>
|
||||
<div id="app">
|
||||
<div id="top_pane">
|
||||
<div id="info_pane">
|
||||
|
||||
</div>
|
||||
<div id="cue_pane">
|
||||
<h2>Preview</h2>
|
||||
<video id="cue_deck" controls />
|
||||
</div>
|
||||
<div id="live_pane">
|
||||
<h2>On-Air</h2>
|
||||
<video id="live_deck" controls />
|
||||
</div>
|
||||
<div id="calendar_pane">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="tabs">
|
||||
<div id="tab_handles">
|
||||
<div id="library_tab" class="tabHandle activeTabHandle">Library</div>
|
||||
<div id="scheduler_tab" class="tabHandle">Scheduler</div>
|
||||
<div id="epg_tab" class="tabHandle">EPG</div>
|
||||
</div>
|
||||
<div id="tab_content">
|
||||
<div id="library" class="tab activeTab">
|
||||
<div id="library_content">
|
||||
<div id="library_left_column">
|
||||
<div id="library_browser">
|
||||
<div id="library_files_tab" class="active_library_tab library_tab"><a href="#">By Files</a></div>
|
||||
<div id="library_tags_tab" class="library_tab"><a href="#">By Tags</a></div>
|
||||
</div>
|
||||
<div id="library_listing">
|
||||
<?php
|
||||
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div id="library_details">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scheduler" class="tab">
|
||||
Scheduler goes here
|
||||
</div>
|
||||
<div id="epg" class="tab">
|
||||
EPG goes here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="upload_target">Drop A Video File Here To Upload</div>
|
||||
<div id="log_console"></div>
|
||||
</div>
|
||||
|
||||
<script language="javascript">
|
||||
// set up info pane
|
||||
const infoPane = document.getElementById("info_pane");
|
||||
function updateInfoPane() {
|
||||
const now = new Date(Date.now());
|
||||
infoPane.innerHTML = now.toDateString() + "<br />" +
|
||||
now.toTimeString();
|
||||
}
|
||||
const infoTimer = setInterval(updateInfoPane, 1000);
|
||||
// set up drag'n'drop file upload
|
||||
const uploadTarget = document.getElementById("upload_target");
|
||||
uploadTarget.addEventListener("dragenter", (event) => {
|
||||
event.target.classList.add("readyToDrop");
|
||||
});
|
||||
uploadTarget.addEventListener("dragleave", (event) => {
|
||||
event.target.classList.remove("readyToDrop");
|
||||
});
|
||||
uploadTarget.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
uploadTarget.addEventListener("drop", (event) => {
|
||||
console.log("Objects dropped on upload target.");
|
||||
event.preventDefault();
|
||||
event.target.classList.remove("readyToDrop");
|
||||
|
||||
if (event.dataTransfer.items) {
|
||||
console.log(event.dataTransfer.items.length + " items dropped.");
|
||||
[...event.dataTransfer.items].forEach((item, i) => {
|
||||
// If dropped items aren't files, reject them
|
||||
if (item.kind === "file") {
|
||||
const file = item.getAsFile();
|
||||
console.log(`… file[${i}].name = ${file.name}`);
|
||||
const uploadURL = "upload.php";
|
||||
let data = new FormData();
|
||||
data.append('file', file);
|
||||
fetch(uploadURL, {
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((json) => {
|
||||
// TODO: add dialog to get media metadata for new upload
|
||||
console.log("Upload response: ");
|
||||
console.dir(json);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Use DataTransfer interface to access the file(s)
|
||||
[...event.dataTransfer.files].forEach((file, i) => {
|
||||
console.log(`… file[${i}].name = ${file.name}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// tab handler
|
||||
const tabs = document.querySelectorAll(".tabHandle");
|
||||
const tabContents = document.querySelectorAll(".tab");
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
console.log("Tab click: " + event.target.id);
|
||||
tabs.forEach((tab) => {
|
||||
if (event.target.id !== tab.id) {
|
||||
tab.classList.remove("activeTabHandle");
|
||||
}
|
||||
});
|
||||
event.target.classList.add("activeTabHandle");
|
||||
tabContents.forEach((content) => {
|
||||
content.classList.remove("activeTab");
|
||||
});
|
||||
const contentTargetId = event.target.id.replace("_tab", "");
|
||||
const contentTarget = document.getElementById(contentTargetId);
|
||||
contentTarget.classList.add("activeTab");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
660
layout.html
Normal file
660
layout.html
Normal file
@ -0,0 +1,660 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NETV MAM</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 16vh;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
height: 74vh;
|
||||
border-top: 3px solid white;
|
||||
border-bottom: 3px solid white;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 2vh;
|
||||
}
|
||||
|
||||
#cue_video, #live_video {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tabs ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
display: inline-block;
|
||||
border: solid;
|
||||
border-width: 1px 1px 0 1px;
|
||||
border-color: white;
|
||||
margin: 0 5px 0 0;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#tabs li a {
|
||||
padding: 0 10px;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#tab_content {
|
||||
display: flex;
|
||||
border-top: 1px solid white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.active_tab {
|
||||
padding-bottom: 1px;
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
#upload_target {
|
||||
position: absolute;
|
||||
bottom: 3vh;
|
||||
left: 0;
|
||||
width: 25vw;
|
||||
height: 10vh;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
.readyToDrop {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
magenta,
|
||||
rebeccapurple,
|
||||
dodgerblue,
|
||||
green
|
||||
);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
#media_library_contents {
|
||||
width: 25vw;
|
||||
height: 52vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#media_item_details {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 3vh;
|
||||
width: 70vw;
|
||||
height: 45vh;
|
||||
border: 1px solid white;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#media_item_preview {
|
||||
width: 720px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#media_item_form {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.media_item_list_element {
|
||||
width: 24vw;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
#media_item_editor_tags {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
border: 1px solid white;
|
||||
padding: 2px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.tag.enabled {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.tag.disabled {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#media_item_edl_list {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#media_item_edl_list label {
|
||||
display: inline-block;
|
||||
width: 5vw;
|
||||
}
|
||||
|
||||
#media_item_edl_list_container {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#media_item_edl_list_add_insert_button {
|
||||
width: 3vw;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<section id="infobox">
|
||||
<h1>NETV MAM</h1>
|
||||
<p id="current_date"></p>
|
||||
<p id="current_time"></p>
|
||||
</section>
|
||||
<!-- we don't need these yet
|
||||
<section id="cue_deck">
|
||||
<video id="cue_video" controls></video>
|
||||
<p>Preview Deck</p>
|
||||
</section>
|
||||
<section id="live_deck">
|
||||
<video id="live_video" controls="off"></video>
|
||||
<p>Live on NETV</p>
|
||||
</section>
|
||||
-->
|
||||
</header>
|
||||
<main>
|
||||
<nav id="tabs">
|
||||
<ul>
|
||||
<li class="active_tab"><a href="#" id="library_tab">Media Library</a></li>
|
||||
<li><a href="#" id="tags_tab">Tag Editor</a></li>
|
||||
<li><a href="#" id="schedules_tab">Schedules</a></li>
|
||||
<li><a href="#" id="epg_tab">EPG</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section id="tab_content">
|
||||
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<section id="colophon">
|
||||
Copyright © 2024 Mountaintown Technology - Version 0.8.0
|
||||
</section>
|
||||
</footer>
|
||||
<template id="media_library_template">
|
||||
<section id="media_library_browser">
|
||||
<h2>Media Files</h2>
|
||||
<section id="upload_target"><p>Drag files here to upload</p></section>
|
||||
<p>Filter: <input type="text" id="media_library_filter" /></p>
|
||||
<section id="media_library_contents">
|
||||
|
||||
</section>
|
||||
<dialog id="media_details_dialog">
|
||||
<form id="media_details_dialog_form" method="dialog" />
|
||||
<h2 id="media_details_dialog_header">Metadata</h2>
|
||||
<p><label for="media_details_dialog_id">ID: </label><input name="id" id="media_details_dialog_id" type="text" disabled /></p>
|
||||
<p><label for="media_details_dialog_title">Title: </label><input name="title" id="media_details_dialog_title" type="text" /></p>
|
||||
<p><label for="media_details_dialog_description">Description: </label><input name="description" id="media_details_dialog_description" type="text" /></p>
|
||||
<p><label for="media_details_dialog_season">Season number: </label><input name="season" id="media_details_dialog_season" type="number" /></p>
|
||||
<p><label for="media_details_dialog_episode_number">Episode number: </label><input name="episode_number" id="media_details_dialog_episode_number" type="number" /></p>
|
||||
<p>Duration: <span id="media_details_dialog_duration_secs"></span></p>
|
||||
<button id="media_details_dialog_save_button" value="save">Save Metadata</button><button id="media_details_dialog_close_button" value="close">Close Without Saving</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</section>
|
||||
<section id="media_library_details">
|
||||
|
||||
</section>
|
||||
</template>
|
||||
<template id="media_item_template">
|
||||
<div class="media_item_list_element">
|
||||
<img class="media_item_thumbnail">
|
||||
<strong class="media_item_title"></strong>
|
||||
<p class="media_item_info"></p>
|
||||
</template>
|
||||
<template id="tag_editor_template">
|
||||
<section id="tag_editor">
|
||||
<h2>Tags</h2>
|
||||
<p>Current tags:</p>
|
||||
<p class="tags_list"></p>
|
||||
<form id="add_tag_form" action="tags.php" method="POST">
|
||||
<p><label for="new_tag">New tag: </label><input type="text" name="new_tag" id="new_tag_input" /></p>
|
||||
<button id="save_tag_button" value="save">Save Tag</button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
<template id="media_item_editor">
|
||||
<div class="media_item_details">
|
||||
<video id="media_item_preview" controls></video>
|
||||
<div id="media_item_form">
|
||||
<form id="media_item_form_data">
|
||||
<p><label for="id">ID: </label><input type="text" name="id" disabled /></p>
|
||||
<p><label for="title">Title: </label><input type="text" name="title" /></p>
|
||||
<p><label for="description">Description: </label><input type="text" name="description" /></p>
|
||||
<p><label for="season">Season number: </label><input type="number" name="season" /></p>
|
||||
<p><label for="episode_number">Episode number: </label><input type="number" name="episode_number" /></p>
|
||||
<p>Tags:</p>
|
||||
<div id="media_item_editor_tags"></div>
|
||||
<button id="media_item_editor_save_button" value="save">Save Metadata</button><button id="media_item_editor_cancel_button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="media_item_edl">
|
||||
<h2>EDL</h2>
|
||||
<div id="media_item_edl_list_container">
|
||||
<div id="media_item_edl_add_insert_container">
|
||||
<input type="button" id="media_item_edl_add_insert_button" name="media_item_edl_add_insert_button" value="Add Insert Point"></button>
|
||||
</div>
|
||||
<ul id="media_item_edl_list">
|
||||
<li id="media_item_edl_inpoint_listitem"><label for="media_item_edl_inpoint">In: </label><input type="text" name="media_item_edl_inpoint" value="00:00:00" /></li>
|
||||
|
||||
<li id="media_item_edl_outpoint_listitem"><label for="media_item_edl_outpoint">Out: </label><input type="text" name="media_item_edl_outpoint" value="" /></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button id="media_item_edl_save_button" value="Save EDL">Save EDL</button>
|
||||
</div>
|
||||
</template>
|
||||
<template id="media_item_edl_insert_listitem">
|
||||
<li class="media_item_edl_insert">
|
||||
<label for="media_item_edl_insert">Insert: </label>
|
||||
<input type="text" name="media_item_edl_insert" />
|
||||
<button class="media_item_edl_remove_insert">X</button>
|
||||
<button class="media_item_edl_jump_insert">-></button>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
const init_library = () => {
|
||||
// set up drag'n'drop file upload handling
|
||||
const upload_target = document.getElementById("upload_target");
|
||||
upload_target.addEventListener("dragenter", (event) => {
|
||||
event.target.classList.add("readyToDrop");
|
||||
});
|
||||
upload_target.addEventListener("dragleave", (event) => {
|
||||
event.target.classList.remove("readyToDrop");
|
||||
});
|
||||
upload_target.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
upload_target.addEventListener("drop", (event) => {
|
||||
console.log("Objects dropped on upload target.");
|
||||
event.preventDefault();
|
||||
event.target.classList.remove("readyToDrop");
|
||||
|
||||
if (event.dataTransfer.items) {
|
||||
console.log(event.dataTransfer.items.length + " items dropped.");
|
||||
[...event.dataTransfer.items].forEach((item, i) => {
|
||||
// If dropped items aren't files, reject them
|
||||
if (item.kind === "file") {
|
||||
const file = item.getAsFile();
|
||||
console.log(`… file[${i}].name = ${file.name}`);
|
||||
const uploadURL = "upload.php";
|
||||
let data = new FormData();
|
||||
data.append('file', file);
|
||||
fetch(uploadURL, {
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((json) => {
|
||||
// show dialog to get media metadata for new upload
|
||||
console.log("Upload response: ");
|
||||
console.dir(json);
|
||||
const metadata_dialog = document.getElementById("media_details_dialog");
|
||||
const metadata_header = document.getElementById("media_details_dialog_header");
|
||||
metadata_header.innerHTML = "Metadata for " + json.source_path;
|
||||
const metadata_title = document.getElementById("media_details_dialog_title");
|
||||
metadata_title.value = json.title === "unknown" ? json.source_path : json.title;
|
||||
const dur_span = document.getElementById("media_details_dialog_duration_secs");
|
||||
dur_span.innerHTML = new Date(json.duration_secs * 1000).toISOString().substring(11, 19);
|
||||
const id_input = document.getElementById("media_details_dialog_id");
|
||||
id_input.value = json.id;
|
||||
const metadata_close_button = document.getElementById("media_details_dialog_close_button");
|
||||
metadata_close_button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
metadata_dialog.close();
|
||||
});
|
||||
const metadata_save_button = document.getElementById("media_details_dialog_save_button");
|
||||
metadata_save_button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
const updateURL = "update.php?id=" + json.id;
|
||||
let data = new FormData(document.getElementById("media_details_dialog_form"));
|
||||
console.dir(data);
|
||||
fetch(updateURL, {
|
||||
method: "POST",
|
||||
body: data
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((json) => {
|
||||
metadata_dialog.close();
|
||||
fetch_media_items();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
metadata_dialog.showModal();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Use DataTransfer interface to access the file(s)
|
||||
[...event.dataTransfer.files].forEach((file, i) => {
|
||||
console.log(`… file[${i}].name = ${file.name}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_media_items = () => {
|
||||
const api_url = "media.php";
|
||||
fetch(api_url).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((rows) => {
|
||||
const media_library_target = document.getElementById("media_library_contents");
|
||||
media_library_target.innerHTML = "";
|
||||
rows.forEach((row) => {
|
||||
const duration = new Date(row['duration_secs'] * 1000).toISOString().substring(11, 19);
|
||||
const template = document.getElementById("media_item_template");
|
||||
const template_clone = template.content.firstElementChild.cloneNode(true);
|
||||
template_clone.querySelector(".media_item_title").innerHTML = row['title'];
|
||||
template_clone.querySelector(".media_item_info").innerHTML = "<p>" + row['description'] + "</p><p>Season " + row['season'] + " Episode " + row['episode_number'] + " - (Duration " + duration + ")</p>";
|
||||
const thisRow = media_library_target.appendChild(template_clone);
|
||||
thisRow.setAttribute("data-id", row['id']);
|
||||
thisRow.setAttribute("data-title", row['title']);
|
||||
thisRow.setAttribute("data-description", row['description']);
|
||||
thisRow.setAttribute("data-season", row['season']);
|
||||
thisRow.setAttribute("data-episode-number", row['episode_number']);
|
||||
thisRow.setAttribute("data-duration", row['duration_secs']);
|
||||
});
|
||||
const nodes = document.querySelectorAll(".media_item_list_element");
|
||||
nodes.forEach((node) => {
|
||||
node.addEventListener("click", (event) => {
|
||||
// open media item details editing pane
|
||||
const media_item = event.target.classList.contains(".media_item_list_element") ? event.target : event.target.closest(".media_item_list_element");
|
||||
edit_media_item(media_item);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const edit_media_item = (media_item_element) => {
|
||||
const template = document.getElementById("media_item_editor");
|
||||
const template_clone = template.content.cloneNode(true);
|
||||
const target = document.querySelector("#media_library_details");
|
||||
const item_id = media_item_element.getAttribute("data-id");
|
||||
template_clone.querySelector('#media_item_preview').setAttribute('src', 'play.php?id=' + item_id);
|
||||
template_clone.querySelector('input[name="id"]').value = item_id;
|
||||
template_clone.querySelector('input[name="title"]').value = media_item_element.getAttribute("data-title");
|
||||
template_clone.querySelector('input[name="description"]').value = media_item_element.getAttribute("data-description");
|
||||
template_clone.querySelector('input[name="season"]').value = media_item_element.getAttribute("data-season");
|
||||
template_clone.querySelector('input[name="episode_number"]').value = media_item_element.getAttribute("data-episode-number");
|
||||
const formatted_duration = new Date(media_item_element.getAttribute("data-duration") * 1000).toISOString().substring(11, 19);
|
||||
template_clone.querySelector('input[name="media_item_edl_outpoint"]').value = formatted_duration;
|
||||
template_clone.firstElementChild.setAttribute("data-duration", media_item_element.getAttribute("data-duration"));
|
||||
target.innerHTML = "";
|
||||
target.appendChild(template_clone);
|
||||
fetch_item_tags(item_id);
|
||||
const save_button = document.getElementById("media_item_editor_save_button");
|
||||
save_button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const api_url = "update.php?id=" + item_id;
|
||||
const item_data = new FormData(document.getElementById("media_item_form_data"));
|
||||
fetch(api_url, {
|
||||
method: "POST",
|
||||
body: item_data
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((json) => {
|
||||
fetch_media_items();
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
const edl_insert_button = document.getElementById("media_item_edl_add_insert_button");
|
||||
edl_insert_button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const current_timecode = document.getElementById("media_item_preview").currentTime;
|
||||
const template = document.getElementById("media_item_edl_insert_listitem");
|
||||
const template_clone = template.content.cloneNode(true);
|
||||
const formatted_timestamp = new Date(current_timecode * 1000).toISOString().substring(11, 19);
|
||||
template_clone.querySelector('input[name="media_item_edl_insert"]').value = formatted_timestamp;
|
||||
template_clone.firstElementChild.setAttribute('data-timestamp', current_timecode);
|
||||
const target = document.querySelector("#media_item_edl_outpoint_listitem");
|
||||
const parent = document.querySelector("#media_item_edl_list");
|
||||
parent.insertBefore(template_clone, target);
|
||||
sort_inserts();
|
||||
const remove_buttons = document.querySelectorAll("button.media_item_edl_remove_insert");
|
||||
remove_buttons.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.target.parentNode.remove();
|
||||
});
|
||||
});
|
||||
const jump_buttons = document.querySelectorAll("button.media_item_edl_jump_insert");
|
||||
jump_buttons.forEach((btn) => {
|
||||
btn.removeEventListener("click", jumpHandler);
|
||||
btn.addEventListener("click", jumpHandler);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const jumpHandler = (e) => {
|
||||
e.preventDefault();
|
||||
const player = document.getElementById("media_item_preview");
|
||||
if (!player.paused) player.pause();
|
||||
player.currentTime = e.target.parentNode.getAttribute("data-timestamp");
|
||||
}
|
||||
|
||||
const sort_inserts = () => {
|
||||
const list_parent = document.querySelector("#media_item_edl_list");
|
||||
let new_parent = list_parent.cloneNode(false);
|
||||
const current_items = list_parent.querySelectorAll("li");
|
||||
let items = [];
|
||||
current_items.forEach((item) => {
|
||||
items.push(item);
|
||||
});
|
||||
items.sort((a, b) => {
|
||||
return a.querySelector("input[type='text']").value > b.querySelector("input[type='text']").value
|
||||
});
|
||||
items.forEach((item) => {
|
||||
new_parent.appendChild(item);
|
||||
});
|
||||
list_parent.parentNode.replaceChild(new_parent, list_parent);
|
||||
}
|
||||
|
||||
const fetch_tags = async () => {
|
||||
const api_url = "tags.php";
|
||||
fetch(api_url).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((tags) => {
|
||||
const container = document.querySelector(".tags_list");
|
||||
container.innerHTML = "";
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
container.innerHTML += "<span class='tag' data-id=" + tags[i]['id'] + ">" + tags[i]['tag'] + "</span>";
|
||||
}
|
||||
tag_editor_handlers();
|
||||
});
|
||||
} else {
|
||||
console.log('err fetching tags:');
|
||||
console.dir(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetch_item_tags = async (media_id) => {
|
||||
const api_url = "tags.php?media_id=" + media_id;
|
||||
fetch(api_url).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((tags) => {
|
||||
const container = document.querySelector("#media_item_editor_tags");
|
||||
container.innerHTML = "";
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
const enabled_class = tags[i]['enabled'] === 'true' ? 'enabled' : 'disabled';
|
||||
const new_span = `<span class='tag ${enabled_class}' data-id='${tags[i]['id']}' data-media-id=${media_id}>${tags[i]['tag']}</span>`;
|
||||
container.innerHTML += new_span;
|
||||
}
|
||||
item_tag_editor_handlers();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// tab handlers
|
||||
document.getElementById("library_tab").addEventListener("click", (e) => {
|
||||
setActiveTab(e);
|
||||
const tab_content = document.getElementById("tab_content");
|
||||
tab_content.innerHTML = "";
|
||||
const template = document.getElementById("media_library_template");
|
||||
const template_clone = template.content.cloneNode(true);
|
||||
tab_content.appendChild(template_clone);
|
||||
// fetch media and populate
|
||||
fetch_media_items();
|
||||
// attach event handlers
|
||||
init_library();
|
||||
});
|
||||
|
||||
document.getElementById("tags_tab").addEventListener("click", (e) => {
|
||||
setActiveTab(e);
|
||||
const tab_content = document.getElementById("tab_content");
|
||||
tab_content.innerHTML = "";
|
||||
const template = document.getElementById("tag_editor_template");
|
||||
const template_clone = template.content.cloneNode(true);
|
||||
tab_content.appendChild(template_clone);
|
||||
fetch_tags();
|
||||
});
|
||||
|
||||
const tag_editor_handlers = () => {
|
||||
document.querySelector("#save_tag_button").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const api_url = "tags.php";
|
||||
fetch(api_url, {
|
||||
method: "POST",
|
||||
body: new FormData(document.getElementById("add_tag_form"))
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((row) => {
|
||||
document.querySelector("#new_tag_input").value = "";
|
||||
fetch_tags();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const item_tag_editor_handlers = () => {
|
||||
const enabled_tags = document.querySelectorAll(".tag.enabled");
|
||||
const disabled_tags = document.querySelectorAll(".tag.disabled");
|
||||
|
||||
enabled_tags.forEach((tag) => {
|
||||
tag.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const tag_id = e.target.getAttribute("data-id");
|
||||
const media_id = e.target.getAttribute("data-media-id");
|
||||
const api_url = "tags.php?enable=false&media_id=" + media_id + "&tag_id=" + tag_id;
|
||||
fetch(api_url).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((json) => {
|
||||
if (json.ok) {
|
||||
fetch_item_tags(media_id);
|
||||
} else {
|
||||
console.dir(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
disabled_tags.forEach((tag) => {
|
||||
tag.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const tag_id = e.target.getAttribute("data-id");
|
||||
const media_id = e.target.getAttribute("data-media-id");
|
||||
const api_url = "tags.php?enable=true&media_id=" + media_id + "&tag_id=" + tag_id;
|
||||
fetch(api_url).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((json) => {
|
||||
if (json.ok) {
|
||||
fetch_item_tags(media_id);
|
||||
} else {
|
||||
console.dir(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("schedules_tab").addEventListener("click", (e) => {
|
||||
setActiveTab(e);
|
||||
document.querySelector("#tab_content").innerHTML = "<h2>Schedules</h2>"
|
||||
+ "<p>Schedules go here.</p>";
|
||||
});
|
||||
|
||||
document.getElementById("epg_tab").addEventListener("click", (e) => {
|
||||
setActiveTab(e);
|
||||
document.querySelector("#tab_content").innerHTML = "<h2>EPG</h2>"
|
||||
+ "<p>EPG goes here.</p>";
|
||||
});
|
||||
|
||||
const setActiveTab = (e) => {
|
||||
e.preventDefault();
|
||||
document.querySelector(".active_tab").classList.remove("active_tab");
|
||||
e.target.classList.add("active_tab");
|
||||
}
|
||||
|
||||
const updateDate = () => {
|
||||
document.getElementById("current_date").innerHTML = new Date().toDateString();
|
||||
}
|
||||
|
||||
const updateTime = () => {
|
||||
document.getElementById("current_time").innerHTML = new Date().toTimeString();
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.getElementById("library_tab").click();
|
||||
updateDate();
|
||||
updateTime();
|
||||
const dateTimer = setInterval(updateDate, 1000 * 60);
|
||||
const timeTimer = setInterval(updateTime, 1000);
|
||||
|
||||
window.addEventListener("dragenter", () => {
|
||||
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
48
media.php
Normal file
48
media.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
$db = new SQLite3('netv-mam.sqlite');
|
||||
|
||||
if (isset($_GET['id']) && $_GET['id'] !== "") {
|
||||
// get info about single file
|
||||
$query = $db->prepare("SELECT * FROM media WHERE id = :id");
|
||||
$query->bindValue(":id", intval($_GET['id']));
|
||||
$result = $query->execute();
|
||||
if ($result) {
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
echo(json_encode($row));
|
||||
}
|
||||
} else if (isset($_GET['filter']) && trim($_GET['filter']) !== "") {
|
||||
// get info about a subset of media files
|
||||
$query = $db->prepare("SELECT * FROM media m
|
||||
JOIN media_tags mt ON m.id = mt.media_id
|
||||
JOIN tags t ON mt.tag_id = t.id
|
||||
WHERE m.title LIKE :filter
|
||||
OR m.description LIKE :filter
|
||||
OR t.tag LIKE :filter
|
||||
ORDER BY m.id DESC");
|
||||
$query->bindValue(":filter", "%" . $_GET['filter'] . "%");
|
||||
error_log($query->getSQL(true));
|
||||
$result = $query->execute();
|
||||
if ($result) {
|
||||
$rows = array();
|
||||
while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$rows[] = $res;
|
||||
}
|
||||
echo(json_encode($rows));
|
||||
} else {
|
||||
error_log($db->lastErrorMsg());
|
||||
}
|
||||
} else {
|
||||
// get info about all media files
|
||||
$query = "SELECT * FROM media ORDER BY id DESC";
|
||||
$result = $db->query($query);
|
||||
if ($result) {
|
||||
$rows = array();
|
||||
while ($res = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$rows[] = $res;
|
||||
}
|
||||
}
|
||||
echo(json_encode($rows));
|
||||
}
|
||||
|
||||
?>
|
29
play.php
Normal file
29
play.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// stream a file from the media dir by db id
|
||||
$db = new SQLite3('netv-mam.sqlite');
|
||||
|
||||
if (isset($_GET['id']) && $_GET['id'] !== '') {
|
||||
$query = $db->prepare("SELECT * FROM media WHERE id = :id");
|
||||
$query->bindValue(':id', intval($_GET['id']));
|
||||
$result = $query->execute();
|
||||
|
||||
if ($result) {
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
$path = $row['source_path'];
|
||||
|
||||
// get the file's mime type to send the correct content type header
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime_type = finfo_file($finfo, $path);
|
||||
$public_name = basename($path);
|
||||
|
||||
// send the headers
|
||||
if (isset($_GET['download']) && $_GET['download'] === "true") { header("Content-Disposition: attachment; filename=$public_name;"); }
|
||||
header("Content-Type: $mime_type");
|
||||
header('Content-Length: ' . filesize($path));
|
||||
|
||||
$fp = fopen($path, 'rb');
|
||||
fpassthru($fp);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
91
tags.php
Normal file
91
tags.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
$db = new SQLite3('netv-mam.sqlite');
|
||||
|
||||
// manage tag data
|
||||
if (isset($_POST['new_tag']) && $_POST['new_tag'] !== "") {
|
||||
// add new tag
|
||||
$query = $db->prepare("INSERT INTO tags (tag) VALUES (:new_tag)");
|
||||
$query->bindValue(":new_tag", $_POST['new_tag']);
|
||||
$result = $query->execute();
|
||||
|
||||
if ($result) {
|
||||
$id = $db->lastInsertRowID();
|
||||
$record = array(
|
||||
"id" => $id,
|
||||
"tag" => $_POST['new_tag']
|
||||
);
|
||||
header('Content-Type: application/json');
|
||||
echo(json_encode($record));
|
||||
exit();
|
||||
}
|
||||
} else if (isset($_GET['enable']) && isset($_GET['media_id']) && intval($_GET['media_id']) > -1 && isset($_GET['tag_id']) && intval($_GET['tag_id']) > -1) {
|
||||
// set or unset tag for media_id
|
||||
if ($_GET['enable'] === "true") {
|
||||
$query = $db->prepare("INSERT INTO media_tags (media_id, tag_id) VALUES (:media_id, :tag_id)");
|
||||
} else {
|
||||
$query = $db->prepare("DELETE FROM media_tags WHERE media_id = :media_id AND tag_id = :tag_id");
|
||||
}
|
||||
$query->bindValue(":media_id", $_GET['media_id']);
|
||||
$query->bindValue(":tag_id", $_GET['tag_id']);
|
||||
$result = $query->execute();
|
||||
|
||||
header("Content-Type: application/json");
|
||||
if ($result) {
|
||||
echo('{"ok": true}');
|
||||
} else {
|
||||
echo('{"ok": false}');
|
||||
}
|
||||
exit();
|
||||
} else if (isset($_GET['media_id']) && intval($_GET['media_id']) > -1) {
|
||||
// get all tags and on/off status for a particular media item
|
||||
$tagsQuery = "SELECT * FROM tags ORDER BY tag ASC";
|
||||
$tagsResult = $db->query($tagsQuery);
|
||||
|
||||
if ($tagsResult) {
|
||||
$tags = array();
|
||||
|
||||
while ($row = $tagsResult->fetchArray(SQLITE3_ASSOC)) {
|
||||
$tags[] = $row;
|
||||
}
|
||||
|
||||
$tagsForMediaQuery = $db->prepare("SELECT * FROM media_tags WHERE media_id = :media_id");
|
||||
$tagsForMediaQuery->bindValue(":media_id", intval($_GET['media_id']));
|
||||
$tagsForMediaResult = $tagsForMediaQuery->execute();
|
||||
|
||||
if ($tagsForMediaResult) {
|
||||
$tagsForMedia = array();
|
||||
|
||||
while ($row = $tagsForMediaResult->fetchArray(SQLITE3_ASSOC)) {
|
||||
$tagsForMedia[] = $row['tag_id'];
|
||||
}
|
||||
|
||||
for ($i = 0; $i < sizeof($tags); $i++) {
|
||||
if (array_search($tags[$i]['id'], $tagsForMedia) !== false) {
|
||||
$tags[$i]['enabled'] = 'true';
|
||||
} else {
|
||||
$tags[$i]['enabled'] = 'false';
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo(json_encode($tags));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// get all tags
|
||||
$query = "SELECT * FROM tags ORDER BY tag ASC";
|
||||
$result = $db->query($query);
|
||||
|
||||
if ($result) {
|
||||
$rows = array();
|
||||
header('Content-Type: application/json');
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo( json_encode($rows) );
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
?>
|
34
update.php
Normal file
34
update.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
$db = new SQLite3('netv-mam.sqlite');
|
||||
|
||||
// update metadata for a file by id
|
||||
if (isset($_GET['id']) && $_GET['id'] !== "") {
|
||||
error_log(print_r($_REQUEST, true));
|
||||
$id = intval($_GET['id']);
|
||||
$query = $db->prepare("SELECT * FROM media WHERE id = :id");
|
||||
$query->bindValue(':id', $id);
|
||||
$result = $query->execute();
|
||||
|
||||
if ($result) {
|
||||
// record exists, update it
|
||||
$update_query = $db->prepare("UPDATE media SET title=:title, description=:description, season=:season, episode_number=:episode_number WHERE id = :id");
|
||||
$update_query->bindValue(':title', $_REQUEST['title']);
|
||||
$update_query->bindValue(':description', $_REQUEST['description']);
|
||||
$update_query->bindValue(':season', $_REQUEST['season']);
|
||||
$update_query->bindValue(':episode_number', $_REQUEST['episode_number']);
|
||||
$update_query->bindValue(':id', $id);
|
||||
error_log($update_query->getSQL(true));
|
||||
$update_result = $update_query->execute();
|
||||
if ($update_result) {
|
||||
$query = $db->prepare("SELECT * FROM media WHERE id = :id");
|
||||
$query->bindValue(':id', $id);
|
||||
$final_result = $query->execute();
|
||||
if ($final_result) {
|
||||
echo(json_encode($final_result->fetchArray(SQLITE3_ASSOC)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
49
upload.php
Normal file
49
upload.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// handle uploads from js frontend
|
||||
ini_set("max_input_time", 6000);
|
||||
ini_set("post_max_size", 0);
|
||||
ini_set("upload_max_filesize", "32G");
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
$db = new SQLite3('netv-mam.sqlite');
|
||||
|
||||
$uploaddir = "/opt/netv-media/";
|
||||
$uploadfile = $uploaddir . basename($_FILES['file']['name']);
|
||||
|
||||
error_log("File upload:");
|
||||
error_log(json_encode($_FILES));
|
||||
|
||||
if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile)) {
|
||||
// get media info
|
||||
$ffprobe = FFMpeg\FFProbe::create();
|
||||
$duration_secs = $ffprobe
|
||||
->format($uploadfile)
|
||||
->get('duration');
|
||||
|
||||
$query = "INSERT INTO media (source_path, title, duration_secs) VALUES (:source_path, 'unknown', :duration_secs)";
|
||||
$statement = $db->prepare($query);
|
||||
$statement->bindValue(':source_path', $uploadfile);
|
||||
$statement->bindValue(':duration_secs', $duration_secs);
|
||||
$result = $statement->execute();
|
||||
if ($result) {
|
||||
$new_media_file = array(
|
||||
'id' => $db->lastInsertRowID(),
|
||||
'title' => 'unknown',
|
||||
'source_path' => $uploadfile,
|
||||
'duration_secs' => $duration_secs,
|
||||
'has_metadata' => false
|
||||
);
|
||||
error_log("Adding media file:");
|
||||
error_log(json_encode($new_media_file));
|
||||
echo(json_encode($new_media_file));
|
||||
} else {
|
||||
error_log("Problem inserting new media into db:");
|
||||
error_log($db->lastErrorMsg());
|
||||
echo("{}");
|
||||
}
|
||||
} else {
|
||||
echo("{}");
|
||||
}
|
||||
|
||||
?>
|
Loading…
Reference in New Issue
Block a user