feat: add videodb media index with Docker stack

- Add videodb PHP/MySQL media collection manager (Blu-ray, DVD, CD)
- Dockerfile: PHP 8.1 + Apache with GD/mysqli/exif extensions
- docker-compose.yml: app on port 6761 + MySQL 8.0 with health checks
- docker-entrypoint.sh: auto-generates config.inc.php from env vars,
  waits for MySQL, initializes DB schema idempotently
- init-db.php: CLI schema installer using app's own prefix_query() logic
- Persistent volumes for DB, cache, and cover images

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-11 09:49:52 +02:00
commit f55c91276e
1230 changed files with 252321 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
/**
* Enhanced Ajax.Autocompleter with support for parameters array and custom completion code
*/
Ajax.Autocompleter2 = Class.create(Autocompleter.Base, {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
// allow parameters array
getUpdatedChoices: function() {
this.startIndicator();
var entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams) {
// if defaultParams is a string, keep as is and don't convert
var defaultParams = Object.isString(this.options.defaultParams) ? this.options.defaultParams : Object.toQueryString(this.options.defaultParams);
this.options.parameters += '&' + defaultParams;
}
new Ajax.Request(this.url, this.options);
},
// allow custom completion code
onComplete: function(request) {
if (this.options.updateContent) {
this.options.updateContent(request);
this.stopIndicator();
}
else this.updateChoices(request.responseText);
}
});

View File

@@ -0,0 +1,110 @@
/**
* Prototype javascript effects for editing
*
* @package Templates
*
* @todo Check if Ajax.Responder could be used
*
* @author Andreas Goetz <cpuidle@gmx.de>
* $Id: edit.js,v 1.10 2009/03/26 22:23:49 andig2 Exp $
*/
/**
* Using global responder to manage indicator handling for multiple requests
*/
Ajax.Responders.register({
onCreate: function() {
if (Ajax.activeRequestCount > 0 && $('indicator1')) {
$('indicator1').setStyle({display: 'inline'});
}
},
onComplete: function() {
if(Ajax.activeRequestCount == 0 && $('indicator1')) {
$('indicator1').setStyle({display: 'none'});
}
}
});
/**
* Title search
*/
function bindTitle()
{
$('title').focus();
// autocompleter is only enabled for new items
if (!$F('imdbID'))
{
new Ajax.Autocompleter("title", "title_choices", "edit.php", {paramName: "ajax_autocomplete_title",
minChars: 3, frequency: 0.8,
afterUpdateElement: function(text, li) {
document.edi.imdbID.value = li.id;
document.edi.lookup1.checked= true;
// start image search
bindImageLookup();
new Ajax.Request('edit.php', {
parameters: {ajax_prefetch_id: li.id},
/*
// inline update
onSuccess: function(transport) {
$('indicator1').setStyle({display: 'none'});
var el = $('u1');
el.update(transport.responseText);
// alert(el.down('div#content',0).innerHTML);
$('content').update(el.down('#content',0).innerHTML);
el.update('');
}
*/
});
}
});
}
}
/**
* Rating
*/
function bindRating(control, control_val, val)
{
var rating = new Control.Rating(control, {updateUrl: false, value: val,
rated: false, multiple: true, max:10, capture: true,
afterChange: function(value) {
$('rating').value = value;
$(control_val).update(value);
rating.options.rated = false;
}
});
}
/**
* Image lookup
*/
function bindImageLookup()
{
if (!$F('title')) return;
new Ajax.Request('lookup.php', {
parameters: {ajax_render: 1, searchtype: 'image', engine: 'google', find: $F('title')},
onSuccess: function(transport, json) {
if (json.count > 0) {
$('imagecontent').update(transport.responseText);
// either img or a
$$('#imagecontent div.thumbnail').invoke('observe', 'click', function(event) {
$('imgurl').value = event.element().readAttribute('url');
Event.stop(event);
});
$('images').setStyle({display: 'block'});
// Effect.BlindDown('images');
}
}
});
}

View File

@@ -0,0 +1,160 @@
/**
* Prototype javascript effects for filtering
*
* @package Templates
* @author Andreas Goetz <cpuidle@gmx.de>
* $Id: index.js,v 1.10 2009/03/26 11:17:21 andig2 Exp $
*/
/**
* Using global responder to manage indicator handling for multiple requests
*/
Ajax.Responders.register({
onCreate: function() {
if (Ajax.activeRequestCount > 0 && $('indicator1')) {
$('indicator1').setStyle({display: 'inline'});
}
},
onComplete: function() {
if(Ajax.activeRequestCount == 0 && $('indicator1')) {
$('indicator1').setStyle({display: 'none'});
}
}
});
/**
* Clean pre-populated text from input controls
*/
function clearInput(id, term_to_clear)
{
el = $(id);
if (el) {
// Clear input if it matches default value
if (el.value == term_to_clear) el.value = '';
// If the value is blank, then put back term
else if (el.value == '') el.value = term_to_clear;
}
}
Event.observe(document, 'dom:loaded', function()
{
/**
* More/less columns
*/
function send_request(event, val) {
$('columns_less').setAttribute('listcolumns', val);
new Ajax.Request('index.php', {
parameters: {ajax_render: 1, listcolumns: val},
onSuccess: function(transport) {
$('content').update(transport.responseText);
}
});
Event.stop(event);
}
$('columns_less').observe('click', function(event) {
var val = $('columns_less').getAttribute('listcolumns');
val = (val > 1) ? val-1 : 1;
send_request(event, val);
});
$('columns_more').observe('click', function(event) {
var val = $('columns_less').getAttribute('listcolumns');
val++;
send_request(event, val);
});
/**
* Render list contents and update item count
*/
function render_list(transport, json) {
$('content').update(transport.responseText);
$('count').update(json.totalresults);
// paged display
if (json.maxpageno) {
$('pageno').update(json.pageno);
$('maxpageno').update(json.maxpageno);
}
}
/**
* Filters
*/
$('showtv').observe('click', function(event) {
new Ajax.Request('index.php', {
parameters: {ajax_render: 1, showtv: ($('showtv').checked) ? 1 : 0},
onSuccess: render_list
});
});
// radio buttons
$$('#filtersalphabet input[type="radio"]').invoke('observe', 'click', function(event) {
new Ajax.Request('index.php', {
parameters: {ajax_render: 1, filter: event.element().value},
onSuccess: render_list
});
});
/*
// seen links
$$('div.list_seen a').invoke('observe', 'click', function(event) {
Event.stop(event);
$('filter'+event.element().readAttribute('filter')).click();
});
*/
/**
* Owner selection
*/
$$('#owner').invoke('observe', 'change', function(event) {
new Ajax.Request('index.php', {
parameters: {ajax_render: 1, owner: $F('owner')},
onSuccess: render_list
});
});
/**
* Media selection
*/
$$('#mediafilter').invoke('observe', 'change', function(event) {
new Ajax.Request('index.php', {
parameters: {ajax_render: 1, mediafilter: $F('mediafilter')},
onSuccess: render_list
});
});
/**
* Order selection
*/
$$('#order').invoke('observe', 'change', function(event) {
new Ajax.Request('index.php', {
parameters: {ajax_render: 1, order: $F('order')},
onSuccess: render_list
});
});
/**
* Quicksearch
*/
$('quicksearch').focus();
new Ajax.Autocompleter("quicksearch", "item_choices", "index.php", {paramName: "ajax_quicksearch",
minChars: 1, frequency: 0.2,
afterUpdateElement: function(text, li) {
document.location = 'show.php?id='+li.id;
}
});
/**
* Enable disabled controls
*/
// $$('.autoenable').invoke('writeAttribute', {disabled: false});
// $('quicksearch').writeAttribute({disabled: false});
});

View File

@@ -0,0 +1,94 @@
/**
* Prototype javascript effects for searching
*
* @package Templates
* @author Andreas Goetz <cpuidle@gmx.de>
* $Id: search.js,v 1.8 2009/04/04 16:26:30 andig2 Exp $
*/
Event.observe(document, 'dom:loaded', function()
{
/**
* Quicksearch
*/
$('q').focus();
// Ajax.Autocompleter2 allows passing in complex search parameters like arrays
new Ajax.Autocompleter("q", "item_choices", "search.php", {
minChars: 2, indicator: 'indicator1', frequency: 0.2,
parameters: collect_parameters({ajax_quicksearch: 1}),
afterUpdateElement: function(text, li) {
document.location = 'show.php?id=' + li.id;
}
});
/**
* Collect form parameters for search fields and genres
*/
function collect_parameters(parameters)
{
var res = Object.toQueryString(parameters);
// owner
if ($('owner')) {
res += '&owner=' + $F('owner');
}
// genres
$$('#genres input').each(function(el) {
if (el.checked) {
res += '&genres[]=' + el.value;
}
});
// fields
$$('#fields option').each(function(el) {
if (el.selected) {
res += '&fields[]=' + el.value;
}
});
return res;
}
/**
* Create submit request
*/
function submit_form(event)
{
// if (!$F('q')) return;
$('indicator1').setStyle({display: 'inline'});
new Ajax.Request('search.php', {
parameters: collect_parameters({ajax_render: 1, q: $F('q')}),
onSuccess: function(transport, json) {
$('indicator1').setStyle({display: 'none'});
$('content').update(transport.responseText);
$('count').update(json.count);
}
});
}
/**
* Search fields
*/
$$('#fields option').invoke('observe', 'click', submit_form);
// toggle all
$('select_all').observe('click', function() {
selectAllFields();
submit_form();
});
/**
* Genres
*/
$$('#genres input').invoke('observe', 'change', submit_form);
/**
* Owner selection
*/
$$('#owner').invoke('observe', 'change', submit_form);
});

View File

@@ -0,0 +1,152 @@
/**
* Prototype javascript effects for showing
*
* @package Templates
* @author Andreas Goetz <cpuidle@gmx.de>
* $Id: show.js,v 1.5 2009/04/04 16:26:30 andig2 Exp $
*/
/**
* Toggle element's visibility
*/
function toggler(el)
{
if ($(el).style.display == 'block')
$(el).setStyle({display: 'none'});
else
$(el).setStyle({display: 'block'});
}
/**
* Using global responder to manage indicator handling for multiple requests
*/
Ajax.Responders.register({
onCreate: function() {
if (Ajax.activeRequestCount > 0 && $('indicator1')) {
$('indicator1').setStyle({display: 'inline'});
}
},
onComplete: function() {
if(Ajax.activeRequestCount == 0 && $('indicator1')) {
$('indicator1').setStyle({display: 'none'});
}
}
});
/**
* Seen
*/
function bindSeen(id)
{
$('seen').observe('click', function(event) {
new Ajax.Request('show.php', {
parameters: {ajax_update: id, seen: ($('seen').checked) ? 1 : 0},
onSuccess: function(transport, json) {
$('seen').checked = json.result;
$('seen_label').setStyle({display: (json.result) ? 'inline' : 'none'});
}
});
});
}
/**
* Rating
*/
function bindRating(id, url, control, control_val, val, editable)
{
var rating = new Control.Rating(control, {updateUrl: url, updateParameterName: 'rating', value: val,
rated: false, multiple: true, max:10, capture: true,
parameters: {ajax_update: id},
afterChange: function(value) {
$(control_val).update(value);
this.options.rated = false;
}.bind(this)
});
if (!editable) rating.disable();
}
/**
* Youtube
*/
function bindYoutube(title)
{
new Ajax.Request('lookup.php', {
parameters: {ajax_render: 1, searchtype: 'trailer', engine: 'youtube', find: title},
onSuccess: function(transport, json) {
if (json.count > 0) {
$('youtube').setStyle({display: 'inline'});
}
}
});
}
/**
* Purchase
*/
function bindPurchase(title, engines)
{
engines.each(function(e)
{
new Ajax.Request('lookup.php', {
parameters: {ajax_render: 1, searchtype: 'purchase', engine: e, find: title},
onSuccess: function(transport, json) {
if (json.count > 0) {
$('purchase').setStyle({display: 'inline'});
$('purchasecontent').insert(transport.responseText);
// $('images').setStyle({display: 'block'});
// Effect.BlindDown('images');
}
}
});
});
}
/**
* Torrent
*/
function bindTorrent(title, engines)
{
engines.each(function(e)
{
new Ajax.Request('lookup.php', {
parameters: {ajax_render: 1, searchtype: 'download', engine: e, find: title},
onSuccess: function(transport, json) {
if (json.count > 0) {
$('torrent').setStyle({display: 'inline'});
$('torrentcontent').insert(transport.responseText);
// $('images').setStyle({display: 'block'});
// Effect.BlindDown('images');
}
}
});
});
}
function bindRenderer(title, type, engines)
{
engines.each(function(e)
{
new Ajax.Request('lookup.php', {
parameters: {ajax_render: 1, searchtype: type, engine: e, find: title},
onSuccess: function(transport, json) {
//alert(this.options);
if (json.count > 0) {
$('purchase').setStyle({display: 'inline'});
$('purchasecontent').insert(transport.responseText);
// $('images').setStyle({display: 'block'});
// Effect.BlindDown('images');
}
}.bind(this)
});
});
}