From e6d224fffb410201ea76e49d34eccb8dc55d659d Mon Sep 17 00:00:00 2001 From: G4Vi Date: Tue, 14 Oct 2025 23:26:30 -0400 Subject: [PATCH 01/15] refactor: move kodi tv out of MHFS::Kodi --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 112 +++++++++++++++++++++++++++++- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 103 +++------------------------ 2 files changed, 121 insertions(+), 94 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index 6892d68..2b5c04c 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -1,10 +1,120 @@ package MHFS::Kodi::TVShows v0.7.0; use 5.014; use strict; use warnings; +use Cwd qw(abs_path); +use Encode qw(decode); +use Feature::Compat::Try; use File::Basename qw(basename); +use MIME::Base64 qw(decode_base64url); +BEGIN { + if( ! (eval "use JSON; 1")) { + eval "use JSON::PP; 1" or die "No implementation of JSON available"; + warn __PACKAGE__.": Using PurePerl version of JSON (JSON::PP)"; + } +} + use MHFS::Kodi::Util qw(html_list_item); -use MIME::Base64 qw(encode_base64url); +use MHFS::Kodi::Season; use MHFS::Kodi::SeasonLite; +use MHFS::Util qw(read_file fold_case read_text_file_lossy); + +sub _read_season_meta { + my ($self, $showid, $seasonid) = @_; + try { + my $bytes = read_file($self->{tvmeta}."/$showid/$seasonid/season.json"); + my $meta = decode_json($bytes); + return $meta; + } catch($e) {} + return; +} + +sub _readtvdir { + my ($self, $tvshows, $source, $b_tvdir) = @_; + my $dh; + if (! opendir ( $dh, $b_tvdir )) { + warn "Error in opening dir $b_tvdir\n"; + return; + } + my @diritems; + while (my $b_filename = readdir($dh)) { + next if(($b_filename eq '.') || ($b_filename eq '..')); + next if(!(-s "$b_tvdir/$b_filename")); + my $filename = decode('UTF-8', $b_filename, Encode::FB_DEFAULT | Encode::LEAVE_SRC); + next if (! -d _ && $filename !~ /\.(?:avi|mkv|mp4|m4v)$/); + if ($filename !~ /^(.+?)(?:[\.\s]+(\d{4}))?[\.\s]+S(?:eason\s)?0*(\d+)/) { + say "suspicious: $filename"; + } + if ($filename =~ /S(?:eason\s)?0*(\d+)\-S(?:eason\s)?0*(\d+)/) { + $self->_readtvdir($tvshows, $source, "$b_tvdir/$b_filename"); + next; + } + my $showname = $1 || $filename; + my $year = $2; + my $season = $3 // 0; + next if (! $showname); + $showname =~ s/\./ /g; + my $showid = fold_case($showname); + if (! $tvshows->{$showid}) { + my %show = (name => $showname, seasons => {}); + my $plot = $self->{tvmeta}."/$showid/plot.txt"; + try { $show{plot} = read_text_file_lossy($plot); } + catch($e) {} + $tvshows->{$showid} = \%show; + } + $tvshows->{$showid}{seasons}{$season} //= { + editions => {}, + do { + my $meta = $self->_read_season_meta($showid, $season); + $meta ? (meta => $meta) : () + }, + }; + $tvshows->{$showid}{seasons}{$season}{editions}{"$source/$b_filename"} = {name => $filename, isdir => (-d _ // 0)+0}; + } + closedir($dh); +} + +sub build_tv_library { + my ($self) = @_; + my $sources = $self->{sources}; + my %tvshows; + foreach my $source (@$sources) { + if ($self->{server}{settings}{SOURCES}{$source}{type} ne 'local') { + warn "skipping source $source, only local implemented"; + next; + } + my $b_tvdir = $self->{server}{settings}{SOURCES}{$source}{folder}; + $self->_readtvdir(\%tvshows, $source, $b_tvdir); + } + $self->{tvshows} = \%tvshows; +} + +sub new { + my ($name, $server, $tvmeta) = @_; + my $self = bless {server => $server, sources => $server->{settings}{MEDIASOURCES}{tv}, tvmeta => $tvmeta}, $name; + $self->build_tv_library(); + $self +} + +sub get_tv_item { + my ($self, $showid, $seasonid, $source, $b64_item) = @_; + my $tvshows = $self->{tvshows}; + exists $tvshows->{$showid} or die "showid $showid does not exist"; + exists $tvshows->{$showid}{seasons}{$seasonid} or die "season $seasonid does not exist"; + my $seasonitem = $tvshows->{$showid}{seasons}{$seasonid}; + my $sourcemap = $self->{server}{settings}{SOURCES}; + # TODO: Instead of updating the tv library here, the library should be updated when new metadata is loaded + if (!exists $seasonitem->{meta}) { + my $meta = $self->_read_season_meta($showid, $seasonid); + $seasonitem->{meta} = $meta if $meta; + } + $source or return bless {season => $seasonitem, id => $seasonid, sourcemap => $sourcemap}, 'MHFS::Kodi::Season'; + $b64_item or die "b64_item not provided"; + my $path = abs_path($self->{server}{settings}{SOURCES}{$source}{folder} .'/' . decode_base64url($b64_item)); + if (!$path || rindex($path, $self->{server}{settings}{SOURCES}{$source}{folder}, 0) != 0 || ! -f $path) { + die "item not found"; + } + {b_path => $path} +} sub Format { my ($tvvshows) = @_; diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index ad16bdd..faea2fa 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -30,93 +30,14 @@ BEGIN { } } -sub _read_season_meta { - my ($self, $showid, $seasonid) = @_; - try { - my $bytes = read_file($self->{tvmeta}."/$showid/$seasonid/season.json"); - my $meta = decode_json($bytes); - return $meta; - } catch($e) {} - return; -} - -sub readtvdir { - my ($self, $tvshows, $source, $b_tvdir) = @_; - my $dh; - if (! opendir ( $dh, $b_tvdir )) { - warn "Error in opening dir $b_tvdir\n"; - return; - } - my @diritems; - while (my $b_filename = readdir($dh)) { - next if(($b_filename eq '.') || ($b_filename eq '..')); - next if(!(-s "$b_tvdir/$b_filename")); - my $filename = decode('UTF-8', $b_filename, Encode::FB_DEFAULT | Encode::LEAVE_SRC); - next if (! -d _ && $filename !~ /\.(?:avi|mkv|mp4|m4v)$/); - if ($filename !~ /^(.+?)(?:[\.\s]+(\d{4}))?[\.\s]+S(?:eason\s)?0*(\d+)/) { - say "suspicious: $filename"; - } - if ($filename =~ /S(?:eason\s)?0*(\d+)\-S(?:eason\s)?0*(\d+)/) { - $self->readtvdir($tvshows, $source, "$b_tvdir/$b_filename"); - next; - } - my $showname = $1 || $filename; - my $year = $2; - my $season = $3 // 0; - next if (! $showname); - $showname =~ s/\./ /g; - my $showid = fold_case($showname); - if (! $tvshows->{$showid}) { - my %show = (name => $showname, seasons => {}); - my $plot = $self->{tvmeta}."/$showid/plot.txt"; - try { $show{plot} = read_text_file_lossy($plot); } - catch($e) {} - $tvshows->{$showid} = \%show; - } - $tvshows->{$showid}{seasons}{$season} //= { - editions => {}, - do { - my $meta = $self->_read_season_meta($showid, $season); - $meta ? (meta => $meta) : () - }, - }; - $tvshows->{$showid}{seasons}{$season}{editions}{"$source/$b_filename"} = {name => $filename, isdir => (-d _ // 0)+0}; - } - closedir($dh); -} - -sub _build_tv_library { - my ($self, $sources) = @_; - my %tvshows; - foreach my $source (@$sources) { - if ($self->{server}{settings}{SOURCES}{$source}{type} ne 'local') { - warn "skipping source $source, only local implemented"; - next; - } - my $b_tvdir = $self->{server}{settings}{SOURCES}{$source}{folder}; - $self->readtvdir(\%tvshows, $source, $b_tvdir); +sub _get_tvshows_instance { + my ($self, $force_reload) = @_; + if (! exists $self->{tvshows}) { + $self->{tvshows} = MHFS::Kodi::TVShows->new($self->{server}, $self->{tvmeta}); + return $self->{tvshows}; } - \%tvshows -} - -sub _get_tv_item { - my ($self, $tvshows, $showid, $seasonid, $source, $b64_item) = @_; - exists $tvshows->{$showid} or die "showid $showid does not exist"; - exists $tvshows->{$showid}{seasons}{$seasonid} or die "season $seasonid does not exist"; - my $seasonitem = $tvshows->{$showid}{seasons}{$seasonid}; - my $sourcemap = $self->{server}{settings}{SOURCES}; - # TODO: Instead of updating the tv library here, the library should be updated when new metadata is loaded - if (!exists $seasonitem->{meta}) { - my $meta = $self->_read_season_meta($showid, $seasonid); - $seasonitem->{meta} = $meta if $meta; - } - $source or return bless {season => $seasonitem, id => $seasonid, sourcemap => $sourcemap}, 'MHFS::Kodi::Season'; - $b64_item or die "b64_item not provided"; - my $path = abs_path($self->{server}{settings}{SOURCES}{$source}{folder} .'/' . decode_base64url($b64_item)); - if (!$path || rindex($path, $self->{server}{settings}{SOURCES}{$source}{folder}, 0) != 0 || ! -f $path) { - die "item not found"; - } - {b_path => $path} + $self->{tvshows}->build_tv_library() if $force_reload; + $self->{tvshows} } # format tv library for kodi http @@ -130,11 +51,7 @@ sub route_tv { return; } }; - # build the tv show library - if(! exists $self->{tvshows} || $request_path eq $kodidir) { - $self->{tvshows} = $self->_build_tv_library($sources); - } - my $tvshows = $self->{tvshows}; + my $tvshows = $self->_get_tvshows_instance($request_path eq $kodidir); my $tvitem; if ($request_path ne $kodidir) { my $fulltvpath = substr($request_path, length($kodidir)+1); @@ -152,7 +69,7 @@ sub route_tv { return; }; try { - $tvitem = $self->_get_tv_item($tvshows, $showid, $season, $source, $b64_item); + $tvitem = $tvshows->get_tv_item($showid, $season, $source, $b64_item); } catch($e) { say "exception $e"; $request->Send404; @@ -168,7 +85,7 @@ sub route_tv { return; } } else { - $tvitem = bless {tvshows => $tvshows}, 'MHFS::Kodi::TVShows'; + $tvitem = $tvshows; } if(exists $request->{qs}{fmt} && $request->{qs}{fmt} eq 'html') { my $buf = $tvitem->TO_HTML; From 0140ba55160beb2e1a9656963cf36411c84e23a1 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Wed, 15 Oct 2025 00:20:58 -0400 Subject: [PATCH 02/15] kodi metadata: load plot from library first --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 22 ++++++++++++++++++++++ App-MHFS/lib/MHFS/Plugin/Kodi.pm | 21 ++++++++++----------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index 2b5c04c..f751e49 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -116,6 +116,28 @@ sub get_tv_item { {b_path => $path} } +sub get_plot { + my ($self, $showid, $seasonid, $episode) = @_; + my $item = $self->{tvshows}; + exists $item->{$showid} or die "showid $showid does not exist"; + $item = $item->{$showid}; + $seasonid // do { + exists $item->{plot} or die "showid $showid does not have plot yet"; + return $item->{plot}; + }; + exists $item->{seasons}{$seasonid} or die "showid $showid season $seasonid does not exist"; + $item = $item->{seasons}{$seasonid}; + exists $item->{meta} or die "showid $showid season $seasonid does have metadata yet"; + my $meta = $item->{meta}; + $episode // do { + exists $meta->{overview} or die "showid $showid season $seasonid does not have plot yet"; + return $meta->{overview}; + }; + $meta = MHFS::Kodi::Season::_get_season_episode($meta, $episode); + exists $meta->{overview} or die "showid $showid season $seasonid episode $episode does not have plot yet"; + $meta->{overview} +} + sub Format { my ($tvvshows) = @_; my @sortedkeys = sort {basename($a) cmp basename($b)} keys %$tvvshows; diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index faea2fa..9bc10b4 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -623,6 +623,15 @@ sub route_metadata { } $medianame = fold_case($medianame); say "mt $mediatype mmt $metadatatype mn $medianame". (defined $season ? " season $season". (defined $episode ? " episode $episode" : '') : ''); + my $tvshows = $self->_get_tvshows_instance() if $mediatype eq 'tv'; + # tv fastest path, grab from the db + if ($mediatype eq 'tv' && $metadatatype eq 'plot') { + try { + my $plot = $tvshows->get_plot($medianame, $season, $episode); + $request->SendText('text/plain; charset=utf-8', $plot); + return; + } catch ($e) {} + } my %allmediaparams = ( 'movies' => { 'meta' => $self->{moviemeta}, 'search' => 'movie', @@ -634,17 +643,7 @@ sub route_metadata { my $b_metadir = $params->{meta} . '/' . encode_utf8($medianame) . (defined $season ? '/'.encode_utf8($season). (defined $episode ? '/'.encode_utf8($episode) : '') : ''); my $b_plotfile = $params->{meta} . '/' . encode_utf8($medianame) . '/'. (defined $season ? encode_utf8($season).'/season.json' : 'plot.txt'); # fast path, check disk - if (defined $season && $metadatatype eq 'plot') { - try { - my $bytes = read_file($b_plotfile); - my $json = decode_json($bytes); - if (defined $episode) { - $json = MHFS::Kodi::Season::_get_season_episode($json, $episode); - } - $request->SendText('text/plain; charset=utf-8', $json->{overview}); - return; - } catch ($e){} - } elsif (-d $b_metadir) { + if (($mediatype ne 'tv' || $metadatatype ne 'plot') && -d $b_metadir) { my %acceptable = ( 'thumb' => ['png', 'jpg'], 'fanart' => ['png', 'jpg'], 'plot' => ['txt']); if(exists $acceptable{$metadatatype}) { foreach my $totry (@{$acceptable{$metadatatype}}) { From 118639068ab62ae415782f422e954946c1da94c7 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Wed, 15 Oct 2025 01:31:42 -0400 Subject: [PATCH 03/15] kodi: metadata, leave season meta saving to MHFS::Kodi::TVShows --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 39 ++++++++++++++++++++----------- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 9 ++----- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index f751e49..f9b37bc 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -2,8 +2,9 @@ package MHFS::Kodi::TVShows v0.7.0; use 5.014; use strict; use warnings; use Cwd qw(abs_path); -use Encode qw(decode); +use Encode qw(decode encode_utf8); use Feature::Compat::Try; +use File::Path qw(make_path); use File::Basename qw(basename); use MIME::Base64 qw(decode_base64url); BEGIN { @@ -16,16 +17,16 @@ BEGIN { use MHFS::Kodi::Util qw(html_list_item); use MHFS::Kodi::Season; use MHFS::Kodi::SeasonLite; -use MHFS::Util qw(read_file fold_case read_text_file_lossy); +use MHFS::Util qw(read_file fold_case read_text_file_lossy write_file); sub _read_season_meta { my ($self, $showid, $seasonid) = @_; try { my $bytes = read_file($self->{tvmeta}."/$showid/$seasonid/season.json"); my $meta = decode_json($bytes); - return $meta; + return (meta => $meta); } catch($e) {} - return; + return (); } sub _readtvdir { @@ -63,10 +64,7 @@ sub _readtvdir { } $tvshows->{$showid}{seasons}{$season} //= { editions => {}, - do { - my $meta = $self->_read_season_meta($showid, $season); - $meta ? (meta => $meta) : () - }, + $self->_read_season_meta($showid, $season), }; $tvshows->{$showid}{seasons}{$season}{editions}{"$source/$b_filename"} = {name => $filename, isdir => (-d _ // 0)+0}; } @@ -102,11 +100,6 @@ sub get_tv_item { exists $tvshows->{$showid}{seasons}{$seasonid} or die "season $seasonid does not exist"; my $seasonitem = $tvshows->{$showid}{seasons}{$seasonid}; my $sourcemap = $self->{server}{settings}{SOURCES}; - # TODO: Instead of updating the tv library here, the library should be updated when new metadata is loaded - if (!exists $seasonitem->{meta}) { - my $meta = $self->_read_season_meta($showid, $seasonid); - $seasonitem->{meta} = $meta if $meta; - } $source or return bless {season => $seasonitem, id => $seasonid, sourcemap => $sourcemap}, 'MHFS::Kodi::Season'; $b64_item or die "b64_item not provided"; my $path = abs_path($self->{server}{settings}{SOURCES}{$source}{folder} .'/' . decode_base64url($b64_item)); @@ -138,6 +131,26 @@ sub get_plot { $meta->{overview} } +# IF NOT EXISTS unless $force_update is true +sub insert_season_metadata { + my ($self, $showid, $seasonid, $metadata, $force_update) = @_; + my $item = $self->{tvshows}; + exists $item->{$showid} or die "showid $showid does not exist"; + $item = $item->{$showid}; + $seasonid // do { + exists $item->{plot} or die "showid $showid does not have plot yet"; + return $item->{plot}; + }; + exists $item->{seasons}{$seasonid} or die "showid $showid season $seasonid does not exist"; + $item = $item->{seasons}{$seasonid}; + return if (exists $item->{meta} && !$force_update); + my $b_metadir = $self->{tvmeta} . '/' . encode_utf8($showid) . '/' . encode_utf8($seasonid); + make_path($b_metadir); + my $bytes = encode_json($metadata); + write_file("$b_metadir/season.json", $bytes); + $item->{meta} = $metadata; +} + sub Format { my ($tvvshows) = @_; my @sortedkeys = sort {basename($a) cmp basename($b)} keys %$tvvshows; diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index 9bc10b4..1ed0ab5 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -628,6 +628,7 @@ sub route_metadata { if ($mediatype eq 'tv' && $metadatatype eq 'plot') { try { my $plot = $tvshows->get_plot($medianame, $season, $episode); + say "fastest path"; $request->SendText('text/plain; charset=utf-8', $plot); return; } catch ($e) {} @@ -641,7 +642,6 @@ sub route_metadata { }); my $params = $allmediaparams{$mediatype}; my $b_metadir = $params->{meta} . '/' . encode_utf8($medianame) . (defined $season ? '/'.encode_utf8($season). (defined $episode ? '/'.encode_utf8($episode) : '') : ''); - my $b_plotfile = $params->{meta} . '/' . encode_utf8($medianame) . '/'. (defined $season ? encode_utf8($season).'/season.json' : 'plot.txt'); # fast path, check disk if (($mediatype ne 'tv' || $metadatatype ne 'plot') && -d $b_metadir) { my %acceptable = ( 'thumb' => ['png', 'jpg'], 'fanart' => ['png', 'jpg'], 'plot' => ['txt']); @@ -672,12 +672,7 @@ sub route_metadata { # find the season and then the episode if applicable my $showid = $json->{id} // die "showid not available"; _TMDB_api_promise($request->{client}{server}, "tv/$showid/season/$season")->then(sub { - if ($metadatatype eq 'plot' || ! -f $b_plotfile) { - make_path($b_metadir); - my $bytes = encode_json($_[0]); - try { write_file($b_plotfile, $bytes) } - catch ($e) { say "wierd, creating file failed?"; } - } + $tvshows->insert_season_metadata($medianame, $season, $_[0], $metadatatype eq 'plot'); $episode // return $_[0]; MHFS::Kodi::Season::_get_season_episode($_[0], $episode) }) From 98987fb17b8f1bc4775111662daee87bb2904d41 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Wed, 15 Oct 2025 02:02:54 -0400 Subject: [PATCH 04/15] kodi: metadata - move saving show plot to MHFS::Kodi::TVShows --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 21 ++++++++++++++++----- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 12 ++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index f9b37bc..fa79af1 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -17,7 +17,7 @@ BEGIN { use MHFS::Kodi::Util qw(html_list_item); use MHFS::Kodi::Season; use MHFS::Kodi::SeasonLite; -use MHFS::Util qw(read_file fold_case read_text_file_lossy write_file); +use MHFS::Util qw(read_file fold_case read_text_file_lossy write_file write_text_file_lossy); sub _read_season_meta { my ($self, $showid, $seasonid) = @_; @@ -137,10 +137,6 @@ sub insert_season_metadata { my $item = $self->{tvshows}; exists $item->{$showid} or die "showid $showid does not exist"; $item = $item->{$showid}; - $seasonid // do { - exists $item->{plot} or die "showid $showid does not have plot yet"; - return $item->{plot}; - }; exists $item->{seasons}{$seasonid} or die "showid $showid season $seasonid does not exist"; $item = $item->{seasons}{$seasonid}; return if (exists $item->{meta} && !$force_update); @@ -151,6 +147,21 @@ sub insert_season_metadata { $item->{meta} = $metadata; } +# IF NOT EXISTS unless $force_update is true +sub insert_show_plot { + my ($self, $showid, $metadata, $force_update) = @_; + exists $metadata->{overview} or die "metadata does not have plot"; + my $plot = $metadata->{overview}; + my $item = $self->{tvshows}; + exists $item->{$showid} or die "showid $showid does not exist"; + $item = $item->{$showid}; + return if (exists $item->{plot} && !$force_update); + my $b_metadir = $self->{tvmeta} . '/' . encode_utf8($showid); + make_path($b_metadir); + write_text_file_lossy("$b_metadir/plot.txt", $plot); + $item->{plot} = $plot; +} + sub Format { my ($tvvshows) = @_; my @sortedkeys = sort {basename($a) cmp basename($b)} keys %$tvvshows; diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index 1ed0ab5..f2ec42b 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -678,10 +678,14 @@ sub route_metadata { }) })->then(sub { # get the metadata - if (! defined $season && ($metadatatype eq 'plot' || ! -f "$b_metadir/plot.txt")) { - make_path($b_metadir); - try { write_text_file_lossy("$b_metadir/plot.txt", $_[0]->{overview}) } - catch ($e) { say "wierd, creating file failed?"; } + if (! defined $season) { + if ($mediatype eq 'tv') { + $tvshows->insert_show_plot($medianame, $_[0], $metadatatype eq 'plot'); + } elsif ($metadatatype eq 'plot' || ! -f "$b_metadir/plot.txt") { + make_path($b_metadir); + try { write_text_file_lossy("$b_metadir/plot.txt", $_[0]->{overview}) } + catch ($e) { say "wierd, creating file failed?"; } + } } if($metadatatype eq 'plot') { $request->SendText('text/plain; charset=utf-8', $_[0]->{overview}); From 6b643eef0affe888e4252ddc1f26727c08d993aa Mon Sep 17 00:00:00 2001 From: G4Vi Date: Thu, 16 Oct 2025 01:11:38 -0400 Subject: [PATCH 05/15] kodi metadata - move fetching tv metadata out to MHFS::Kodi::TVShows --- App-MHFS/lib/MHFS/HTTP/Server.pm | 1 + App-MHFS/lib/MHFS/Kodi/TVShows.pm | 81 +++++++++++++++++++++++++++++++ App-MHFS/lib/MHFS/Plugin/Kodi.pm | 18 +++++++ 3 files changed, 100 insertions(+) diff --git a/App-MHFS/lib/MHFS/HTTP/Server.pm b/App-MHFS/lib/MHFS/HTTP/Server.pm index d4b3c7d..ed964ba 100644 --- a/App-MHFS/lib/MHFS/HTTP/Server.pm +++ b/App-MHFS/lib/MHFS/HTTP/Server.pm @@ -87,6 +87,7 @@ sub new { # load the plugins foreach my $pluginname (@{$plugins}) { eval "use $pluginname; 1;" or do { + say $@; say __PACKAGE__.": module $pluginname not found!"; next; }; diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index fa79af1..c443b23 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -17,6 +17,7 @@ BEGIN { use MHFS::Kodi::Util qw(html_list_item); use MHFS::Kodi::Season; use MHFS::Kodi::SeasonLite; +use MHFS::Promise; use MHFS::Util qw(read_file fold_case read_text_file_lossy write_file write_text_file_lossy); sub _read_season_meta { @@ -162,6 +163,86 @@ sub insert_show_plot { $item->{plot} = $plot; } +# returns a promise +sub fetch_metadata { + my ($self, $metadatatype, $medianame, $season, $episode) = @_; + # tv fastest path, grab from the db + if ($metadatatype eq 'plot') { + try { + my $plot = $self->get_plot($medianame, $season, $episode); + say "fastest path"; + my $result = {text => $plot}; + return MHFS::Promise->new($self->{server}{evp}, sub { + my ($resolve, $reject) = @_; + $resolve->($result); + }); + } catch ($e) {} + } + my $b_metadir = $self->{tvmeta} . '/' . encode_utf8($medianame) . (defined $season ? '/'.encode_utf8($season). (defined $episode ? '/'.encode_utf8($episode) : '') : ''); + # fast path, check disk + if ($metadatatype ne 'plot' && -d $b_metadir) { + my %acceptable = ( 'thumb' => ['png', 'jpg'], 'fanart' => ['png', 'jpg']); + if (exists $acceptable{$metadatatype}) { + foreach my $totry (@{$acceptable{$metadatatype}}) { + my $path = $b_metadir.'/'.$metadatatype.".$totry"; + if (-f $path) { + return MHFS::Promise->new($self->{server}{evp}, sub { + my ($resolve, $reject) = @_; + $resolve->({file => $path}); + }); + } + } + } + } + # slow path, download it + $self->{server}{settings}{TMDB} or die "TMDB config not available"; + # find the movie or tv show + my $searchname = $medianame; + say "searchname $searchname"; + return MHFS::Plugin::Kodi::_TMDB_api_promise($self->{server}, 'search/tv', {'query' => $searchname})->then(sub { + my $json = $_[0]->{results}[0]; + $json or die "Failed to find item"; + $season // return $json; + # find the season and then the episode if applicable + my $showid = $json->{id} // die "showid not available"; + MHFS::Plugin::Kodi::_TMDB_api_promise($self->{server}, "tv/$showid/season/$season")->then(sub { + $self->insert_season_metadata($medianame, $season, $_[0], $metadatatype eq 'plot'); + $episode // return $_[0]; + MHFS::Kodi::Season::_get_season_episode($_[0], $episode) + }) + })->then(sub { + # get the metadata + if (! defined $season) { + $self->insert_show_plot($medianame, $_[0], $metadatatype eq 'plot'); + } + if($metadatatype eq 'plot') { + return {text => $_[0]->{overview}}; + } + # thumb or fanart + my $imagepartial = ($metadatatype eq 'thumb') ? (! defined $episode ? $_[0]->{poster_path} : $_[0]->{still_path}) : $_[0]->{backdrop_path}; + if (!$imagepartial || $imagepartial !~ /(\.[^\.]+)$/) { + die 'path not matched '.$imagepartial; + } + my $ext = $1; + make_path($b_metadir); + return MHFS::Promise->new($self->{server}{evp}, sub { + my ($resolve, $reject) = @_; + if(! defined $self->{tmdbconfig}) { + $resolve->(MHFS::Plugin::Kodi::_TMDB_api_promise($self->{server}, 'configuration')->then( sub { + $self->{tmdbconfig} = $_[0]; + return $_[0]; + })); + } else { + $resolve->(); + } + })->then( sub { + return MHFS::Plugin::Kodi::_DownloadFile_promise($self->{server}, $self->{tmdbconfig}{images}{secure_base_url}.'original'.$imagepartial, "$b_metadir/$metadatatype$ext")->then(sub { + return {file => "$b_metadir/$metadatatype$ext"}; + }); + }); + }); +} + sub Format { my ($tvvshows) = @_; my @sortedkeys = sort {basename($a) cmp basename($b)} keys %$tvvshows; diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index f2ec42b..6abae74 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -623,6 +623,24 @@ sub route_metadata { } $medianame = fold_case($medianame); say "mt $mediatype mmt $metadatatype mn $medianame". (defined $season ? " season $season". (defined $episode ? " episode $episode" : '') : ''); + if ($mediatype eq 'tv') { + weaken($request); + $self->_get_tvshows_instance()->fetch_metadata($metadatatype, $medianame, $season, $episode)->then(sub { + my ($result) = @_; + if ($result->{file}) { + $request->SendLocalFile($result->{file}); + } elsif ($result->{text}) { + $request->SendText('text/plain; charset=utf-8', $result->{text}); + } else { + die "unknown result type"; + } + })->then(undef, sub { + print $_[0]; + say "fetch_metadata failure"; + $request->Send404; + }); + return; + } my $tvshows = $self->_get_tvshows_instance() if $mediatype eq 'tv'; # tv fastest path, grab from the db if ($mediatype eq 'tv' && $metadatatype eq 'plot') { From 71530711c0445e8eb7c8b74aedb641f000e0694e Mon Sep 17 00:00:00 2001 From: G4Vi Date: Thu, 16 Oct 2025 02:36:53 -0400 Subject: [PATCH 06/15] feat: MHFS::TMDBClient --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 32 +++----- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 13 ++- App-MHFS/lib/MHFS/TMDBClient.pm | 126 ++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 App-MHFS/lib/MHFS/TMDBClient.pm diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index c443b23..7ad89dc 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -88,8 +88,11 @@ sub build_tv_library { } sub new { - my ($name, $server, $tvmeta) = @_; - my $self = bless {server => $server, sources => $server->{settings}{MEDIASOURCES}{tv}, tvmeta => $tvmeta}, $name; + my ($name, $server, $tvmeta, $tmdb_client) = @_; + my %optional = ( + ($tmdb_client ? (tmdb => $tmdb_client) : ()), + ); + my $self = bless {server => $server, sources => $server->{settings}{MEDIASOURCES}{tv}, tvmeta => $tvmeta, %optional}, $name; $self->build_tv_library(); $self } @@ -195,17 +198,18 @@ sub fetch_metadata { } } # slow path, download it - $self->{server}{settings}{TMDB} or die "TMDB config not available"; + exists $self->{tmdb} or die "cannot load metadata without tmdb"; + my $tmdb = $self->{tmdb}; # find the movie or tv show my $searchname = $medianame; say "searchname $searchname"; - return MHFS::Plugin::Kodi::_TMDB_api_promise($self->{server}, 'search/tv', {'query' => $searchname})->then(sub { + return $tmdb->search('tv', {'query' => $searchname})->then(sub { my $json = $_[0]->{results}[0]; $json or die "Failed to find item"; $season // return $json; # find the season and then the episode if applicable my $showid = $json->{id} // die "showid not available"; - MHFS::Plugin::Kodi::_TMDB_api_promise($self->{server}, "tv/$showid/season/$season")->then(sub { + $tmdb->get_tv_season($showid, $season)->then(sub { $self->insert_season_metadata($medianame, $season, $_[0], $metadatatype eq 'plot'); $episode // return $_[0]; MHFS::Kodi::Season::_get_season_episode($_[0], $episode) @@ -225,21 +229,9 @@ sub fetch_metadata { } my $ext = $1; make_path($b_metadir); - return MHFS::Promise->new($self->{server}{evp}, sub { - my ($resolve, $reject) = @_; - if(! defined $self->{tmdbconfig}) { - $resolve->(MHFS::Plugin::Kodi::_TMDB_api_promise($self->{server}, 'configuration')->then( sub { - $self->{tmdbconfig} = $_[0]; - return $_[0]; - })); - } else { - $resolve->(); - } - })->then( sub { - return MHFS::Plugin::Kodi::_DownloadFile_promise($self->{server}, $self->{tmdbconfig}{images}{secure_base_url}.'original'.$imagepartial, "$b_metadir/$metadatatype$ext")->then(sub { - return {file => "$b_metadir/$metadatatype$ext"}; - }); - }); + $tmdb->get_image("original$imagepartial", "$b_metadir/$metadatatype$ext")->then(sub { + {file => $_[0]} + }) }); } diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index 6abae74..c9d5ad0 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -21,6 +21,7 @@ use MHFS::Kodi::MovieSubtitle; use MHFS::Kodi::Season; use MHFS::Process; use MHFS::Promise; +use MHFS::TMDBClient; use MHFS::Util qw(base64url_to_str str_to_base64url uri_escape_path_utf8 read_text_file_lossy write_text_file_lossy decode_utf_8 escape_html_noquote fold_case write_file read_file); use Feature::Compat::Try; BEGIN { @@ -30,10 +31,20 @@ BEGIN { } } +sub _get_tmdb_instance { + my ($self) = @_; + exists $self->{server}{settings}{TMDB} or die "no TMDB api key set"; + my $api_key = $self->{server}{settings}{TMDB}; + $self->{tmdb} //= MHFS::TMDBClient->new($self->{server}, $api_key) +} + sub _get_tvshows_instance { my ($self, $force_reload) = @_; + my $tmdb; + try {$tmdb = $self->_get_tmdb_instance()} + catch ($e){print $e} if (! exists $self->{tvshows}) { - $self->{tvshows} = MHFS::Kodi::TVShows->new($self->{server}, $self->{tvmeta}); + $self->{tvshows} = MHFS::Kodi::TVShows->new($self->{server}, $self->{tvmeta}, $tmdb); return $self->{tvshows}; } $self->{tvshows}->build_tv_library() if $force_reload; diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm new file mode 100644 index 0000000..add13b1 --- /dev/null +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -0,0 +1,126 @@ +package MHFS::TMDBClient v0.7.0; +use 5.014; +use strict; use warnings; +use feature 'say'; +use Encode qw(encode_utf8); +use URI::Escape qw(uri_escape); +BEGIN { + if( ! (eval "use JSON; 1")) { + eval "use JSON::PP; 1" or die "No implementation of JSON available"; + warn __PACKAGE__.": Using PurePerl version of JSON (JSON::PP)"; + } +} + +use MHFS::Promise; + +sub new { + my ($name, $server, $api_key) = @_; + bless {server => $server, api_key => $api_key}, $name +} + +sub _curl { + my ($server, $params, $cb) = @_; + my $process; + my @cmd = ('curl', @$params); + print "$_ " foreach @cmd; + print "\n"; + $process = MHFS::Process->new_io_process($server->{evp}, \@cmd, sub { + my ($output, $error) = @_; + $cb->($output); + }); + + if(! $process) { + $cb->(undef); + } + + return $process; +} + +sub _TMDB_api { + my ($server, $route, $qs, $cb) = @_; + my $url = 'https://api.themoviedb.org/3/' . $route; + $url .= '?api_key=' . $server->{settings}{TMDB} . '&'; + if($qs){ + foreach my $key (keys %{$qs}) { + my @values; + if(ref($qs->{$key}) ne 'ARRAY') { + push @values, $qs->{$key}; + } + else { + @values = @{$qs->{$key}}; + } + foreach my $value (@values) { + $url .= uri_escape($key).'='.uri_escape($value) . '&'; + } + } + } + chop $url; + return _curl($server, [encode_utf8($url)], sub { + $cb->(decode_json($_[0])); + }); +} + +sub _TMDB_api_promise { + my ($server, $route, $qs) = @_; + return MHFS::Promise->new($server->{evp}, sub { + my ($resolve, $reject) = @_; + _TMDB_api($server, $route, $qs, sub { + $resolve->($_[0]); + }); + }); +} + +sub _DownloadFile { + my ($server, $url, $dest, $cb) = @_; + return _curl($server, ['-k', $url, '-o', $dest], $cb); +} + +sub _DownloadFile_promise { + my ($server, $url, $dest) = @_; + return MHFS::Promise->new($server->{evp}, sub { + my ($resolve, $reject) = @_; + _DownloadFile($server, $url, $dest, sub { + $resolve->(); + }); + }); +} + +# returns a promise +sub search { + my ($self, $mediatype, $query) = @_; + _TMDB_api_promise($self->{server}, "search/$mediatype", $query) +} + +# returns a promise +sub get_tv_season { + my ($self, $series_id, $season, $query) = @_; + _TMDB_api_promise($self->{server}, "tv/$series_id/season/$season", $query) +} + +sub _get_config { + my ($self) = @_; + MHFS::Promise->new($self->{server}{evp}, sub { + my ($resolve, $reject) = @_; + if(! defined $self->{tmdbconfig}) { + $resolve->(_TMDB_api_promise($self->{server}, 'configuration')->then( sub { + $self->{tmdbconfig} = $_[0]; + return $_[0]; + })); + } else { + $resolve->($self->{tmdbconfig}); + } + }) +} + +# returns a promise to path +sub get_image { + my ($self, $image_path, $save_path) = @_; + $self->_get_config()->then(sub { + my ($config) = @_; + _DownloadFile_promise($self->{server}, $config->{images}{secure_base_url}.$image_path, $save_path)->then(sub { + $save_path + }) + }) +} + +1; From 4fdde2a604c0768d7448802f26a9a4cb8a334311 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Fri, 17 Oct 2025 02:01:15 -0400 Subject: [PATCH 07/15] kodi tvshows metadata abstract image loading further --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 17 ++++------------- App-MHFS/lib/MHFS/TMDBClient.pm | 13 +++++++++++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index 7ad89dc..69d5080 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -206,6 +206,7 @@ sub fetch_metadata { return $tmdb->search('tv', {'query' => $searchname})->then(sub { my $json = $_[0]->{results}[0]; $json or die "Failed to find item"; + $self->insert_show_plot($medianame, $json, ! defined $season && $metadatatype eq 'plot'); $season // return $json; # find the season and then the episode if applicable my $showid = $json->{id} // die "showid not available"; @@ -215,21 +216,11 @@ sub fetch_metadata { MHFS::Kodi::Season::_get_season_episode($_[0], $episode) }) })->then(sub { - # get the metadata - if (! defined $season) { - $self->insert_show_plot($medianame, $_[0], $metadatatype eq 'plot'); - } - if($metadatatype eq 'plot') { + if ($metadatatype eq 'plot') { return {text => $_[0]->{overview}}; } - # thumb or fanart - my $imagepartial = ($metadatatype eq 'thumb') ? (! defined $episode ? $_[0]->{poster_path} : $_[0]->{still_path}) : $_[0]->{backdrop_path}; - if (!$imagepartial || $imagepartial !~ /(\.[^\.]+)$/) { - die 'path not matched '.$imagepartial; - } - my $ext = $1; - make_path($b_metadir); - $tmdb->get_image("original$imagepartial", "$b_metadir/$metadatatype$ext")->then(sub { + my $type = defined $season ? (defined $episode ? 'tv_episode' : 'tv_season') : 'tv_show'; + $tmdb->get_image_from_metadata($type, $_[0], $metadatatype, $b_metadir)->then(sub { {file => $_[0]} }) }); diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm index add13b1..2201ae2 100644 --- a/App-MHFS/lib/MHFS/TMDBClient.pm +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -2,6 +2,7 @@ package MHFS::TMDBClient v0.7.0; use 5.014; use strict; use warnings; use feature 'say'; +use File::Path qw(make_path); use Encode qw(encode_utf8); use URI::Escape qw(uri_escape); BEGIN { @@ -123,4 +124,16 @@ sub get_image { }) } +# returns a promise to path +sub get_image_from_metadata { + my ($self, $metadata_type, $metadata, $image_type, $destdir) = @_; + my $imagepartial = ($image_type eq 'thumb') ? ($metadata_type ne 'tv_episode' ? $metadata->{poster_path} : $metadata->{still_path}) : $metadata->{backdrop_path}; + if (!$imagepartial || $imagepartial !~ /(\.[^\.]+)$/) { + die 'path not matched '.$imagepartial; + } + my $ext = $1; + make_path($destdir); + $self->get_image("original$imagepartial", "$destdir/$image_type$ext") +} + 1; From a49e7f80c8449da46563bcdac28f0520fbc0ce85 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Fri, 17 Oct 2025 23:46:27 -0400 Subject: [PATCH 08/15] kodi metadata: remove kodi metadata internals from TMDBClient --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 4 ++-- App-MHFS/lib/MHFS/TMDBClient.pm | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index 69d5080..2355dc2 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -219,8 +219,8 @@ sub fetch_metadata { if ($metadatatype eq 'plot') { return {text => $_[0]->{overview}}; } - my $type = defined $season ? (defined $episode ? 'tv_episode' : 'tv_season') : 'tv_show'; - $tmdb->get_image_from_metadata($type, $_[0], $metadatatype, $b_metadir)->then(sub { + my $image_type = ($metadatatype eq 'thumb') ? (! defined $episode ? 'poster_path' : 'still_path') : 'backdrop_path'; + $tmdb->get_image_from_metadata($_[0], $image_type, $b_metadir, $metadatatype)->then(sub { {file => $_[0]} }) }); diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm index 2201ae2..095fba4 100644 --- a/App-MHFS/lib/MHFS/TMDBClient.pm +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -126,14 +126,12 @@ sub get_image { # returns a promise to path sub get_image_from_metadata { - my ($self, $metadata_type, $metadata, $image_type, $destdir) = @_; - my $imagepartial = ($image_type eq 'thumb') ? ($metadata_type ne 'tv_episode' ? $metadata->{poster_path} : $metadata->{still_path}) : $metadata->{backdrop_path}; - if (!$imagepartial || $imagepartial !~ /(\.[^\.]+)$/) { - die 'path not matched '.$imagepartial; - } - my $ext = $1; + my ($self, $metadata, $image_type, $destdir, $save_base) = @_; + exists $metadata->{$image_type} or die "$image_type does not exist in metadata"; + my $imagepartial = $metadata->{$image_type}; + my ($ext) = $imagepartial =~ /(\.[^\.]+)$/ or die "file extension not found in $imagepartial"; make_path($destdir); - $self->get_image("original$imagepartial", "$destdir/$image_type$ext") + $self->get_image("original$imagepartial", "$destdir/$save_base$ext") } 1; From 8ab80b9243d15500df51268f541805349feefb43 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Sat, 18 Oct 2025 00:51:23 -0400 Subject: [PATCH 09/15] _DownloadFile_promise: reject when exit_code is non zero --- App-MHFS/lib/MHFS/Process.pm | 2 +- App-MHFS/lib/MHFS/TMDBClient.pm | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/App-MHFS/lib/MHFS/Process.pm b/App-MHFS/lib/MHFS/Process.pm index aa1c29c..c6106fb 100644 --- a/App-MHFS/lib/MHFS/Process.pm +++ b/App-MHFS/lib/MHFS/Process.pm @@ -256,7 +256,7 @@ sub new_io_process { 'at_exit' => sub { my ($context) = @_; say 'run handler'; - $handler->($context->{'stdout'}, $context->{'stderr'}); + $handler->($context->{'stdout'}, $context->{'stderr'}, $context->{exit_status}); } }; if(defined $inputdata) { diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm index 095fba4..7d4b1ba 100644 --- a/App-MHFS/lib/MHFS/TMDBClient.pm +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -26,8 +26,8 @@ sub _curl { print "$_ " foreach @cmd; print "\n"; $process = MHFS::Process->new_io_process($server->{evp}, \@cmd, sub { - my ($output, $error) = @_; - $cb->($output); + my ($output, $error, $exit_status) = @_; + $cb->($output, $exit_status >>= 8); }); if(! $process) { @@ -73,7 +73,7 @@ sub _TMDB_api_promise { sub _DownloadFile { my ($server, $url, $dest, $cb) = @_; - return _curl($server, ['-k', $url, '-o', $dest], $cb); + return _curl($server, ['-f', $url, '-o', $dest], $cb); } sub _DownloadFile_promise { @@ -81,6 +81,11 @@ sub _DownloadFile_promise { return MHFS::Promise->new($server->{evp}, sub { my ($resolve, $reject) = @_; _DownloadFile($server, $url, $dest, sub { + my ($output, $exit_code) = @_; + if ($exit_code != 0) { + $reject->("download failed exitcode $exit_code\n"); + return; + } $resolve->(); }); }); From 12aa205b4d7b83e2b15dc7713da5e43903cf6555 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Mon, 20 Oct 2025 02:24:46 -0400 Subject: [PATCH 10/15] improve tmdbclient error handling, MHFS::Promise - reject promise if exception is thrown in initial callback --- App-MHFS/lib/MHFS/Promise.pm | 6 +- App-MHFS/lib/MHFS/TMDBClient.pm | 110 +++++++++++++------------------- 2 files changed, 51 insertions(+), 65 deletions(-) diff --git a/App-MHFS/lib/MHFS/Promise.pm b/App-MHFS/lib/MHFS/Promise.pm index bf11ecc..9bea924 100644 --- a/App-MHFS/lib/MHFS/Promise.pm +++ b/App-MHFS/lib/MHFS/Promise.pm @@ -46,7 +46,11 @@ sub _new { sub new { my ($class, $evp, $cb) = @_; my $self = _new(@_); - $cb->($self->{fulfill}, $self->{reject}); + try { + $cb->($self->{fulfill}, $self->{reject}); + } catch ($e) { + $self->{reject}->($e); + } return $self; } diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm index 7d4b1ba..4ce656e 100644 --- a/App-MHFS/lib/MHFS/TMDBClient.pm +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -16,91 +16,73 @@ use MHFS::Promise; sub new { my ($name, $server, $api_key) = @_; + $api_key //= $server->{settings}{TMDB}; bless {server => $server, api_key => $api_key}, $name } -sub _curl { - my ($server, $params, $cb) = @_; - my $process; - my @cmd = ('curl', @$params); - print "$_ " foreach @cmd; - print "\n"; - $process = MHFS::Process->new_io_process($server->{evp}, \@cmd, sub { - my ($output, $error, $exit_status) = @_; - $cb->($output, $exit_status >>= 8); - }); - - if(! $process) { - $cb->(undef); - } - - return $process; -} - -sub _TMDB_api { - my ($server, $route, $qs, $cb) = @_; - my $url = 'https://api.themoviedb.org/3/' . $route; - $url .= '?api_key=' . $server->{settings}{TMDB} . '&'; - if($qs){ - foreach my $key (keys %{$qs}) { - my @values; - if(ref($qs->{$key}) ne 'ARRAY') { - push @values, $qs->{$key}; - } - else { - @values = @{$qs->{$key}}; - } - foreach my $value (@values) { - $url .= uri_escape($key).'='.uri_escape($value) . '&'; - } +sub _curl_Promise { + my ($server, $params) = @_; + MHFS::Promise->new($server->{evp}, sub { + my ($resolve, $reject) = @_; + my @cmd = ('curl', @$params); + print "$_ " foreach @cmd; + print "\n"; + my $process = MHFS::Process->new_io_process($server->{evp}, \@cmd, sub { + my ($output, $error, $exit_status) = @_; + $resolve->({exit_code => $exit_status >> 8, stdout => $output, stderr => $error}); + }); + if(! $process) { + $reject->('failed to start process'); } - } - chop $url; - return _curl($server, [encode_utf8($url)], sub { - $cb->(decode_json($_[0])); - }); + }) } sub _TMDB_api_promise { - my ($server, $route, $qs) = @_; - return MHFS::Promise->new($server->{evp}, sub { + my ($self, $route, $qs) = @_; + MHFS::Promise->new($self->{server}{evp}, sub { my ($resolve, $reject) = @_; - _TMDB_api($server, $route, $qs, sub { - $resolve->($_[0]); - }); - }); -} - -sub _DownloadFile { - my ($server, $url, $dest, $cb) = @_; - return _curl($server, ['-f', $url, '-o', $dest], $cb); + my $url = 'https://api.themoviedb.org/3/' . $route; + $url .= '?api_key=' . $self->{api_key} . '&'; + if($qs){ + foreach my $key (keys %{$qs}) { + my @values; + if(ref($qs->{$key}) ne 'ARRAY') { + push @values, $qs->{$key}; + } + else { + @values = @{$qs->{$key}}; + } + foreach my $value (@values) { + $url .= uri_escape($key).'='.uri_escape($value) . '&'; + } + } + } + chop $url; + $resolve->(_curl_Promise($self->{server}, ['-f', encode_utf8($url)])->then(sub { + $_[0]->{exit_code} == 0 or die "curl to $url failed with " . $_[0]->{exit_code}; + decode_json($_[0]->{stdout}) + })); + }) } sub _DownloadFile_promise { my ($server, $url, $dest) = @_; - return MHFS::Promise->new($server->{evp}, sub { - my ($resolve, $reject) = @_; - _DownloadFile($server, $url, $dest, sub { - my ($output, $exit_code) = @_; - if ($exit_code != 0) { - $reject->("download failed exitcode $exit_code\n"); - return; - } - $resolve->(); - }); - }); + _curl_Promise($server, ['-f', $url, '-o', $dest])->then(sub { + $_[0]->{exit_code} == 0 or die "curl to $url failed with " . $_[0]->{exit_code}; + undef + }) } # returns a promise sub search { my ($self, $mediatype, $query) = @_; - _TMDB_api_promise($self->{server}, "search/$mediatype", $query) + $self->_TMDB_api_promise("search/$mediatype", $query) } # returns a promise sub get_tv_season { my ($self, $series_id, $season, $query) = @_; - _TMDB_api_promise($self->{server}, "tv/$series_id/season/$season", $query) + $self->_TMDB_api_promise("tv/$series_id/season/$season", $query) } sub _get_config { @@ -108,7 +90,7 @@ sub _get_config { MHFS::Promise->new($self->{server}{evp}, sub { my ($resolve, $reject) = @_; if(! defined $self->{tmdbconfig}) { - $resolve->(_TMDB_api_promise($self->{server}, 'configuration')->then( sub { + $resolve->($self->_TMDB_api_promise('configuration')->then( sub { $self->{tmdbconfig} = $_[0]; return $_[0]; })); From 9b0dbd8f25e7b6710e7b54d55bd360b07cc361eb Mon Sep 17 00:00:00 2001 From: G4Vi Date: Wed, 22 Oct 2025 01:37:49 -0400 Subject: [PATCH 11/15] introduce MHFS::Promise::try to make wrapping with a Promise easier --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 9 +++++++-- App-MHFS/lib/MHFS/Promise.pm | 19 +++++++++++++++++++ App-MHFS/lib/MHFS/TMDBClient.pm | 12 +++++++----- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index 2355dc2..ffe0095 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -166,8 +166,7 @@ sub insert_show_plot { $item->{plot} = $plot; } -# returns a promise -sub fetch_metadata { +sub _fetch_metadata { my ($self, $metadatatype, $medianame, $season, $episode) = @_; # tv fastest path, grab from the db if ($metadatatype eq 'plot') { @@ -226,6 +225,12 @@ sub fetch_metadata { }); } +# returns a promise +sub fetch_metadata { + # Look Ma, no closure + MHFS::Promise::try($_[0]->{server}{evp}, \&_fetch_metadata, @_) +} + sub Format { my ($tvvshows) = @_; my @sortedkeys = sort {basename($a) cmp basename($b)} keys %$tvvshows; diff --git a/App-MHFS/lib/MHFS/Promise.pm b/App-MHFS/lib/MHFS/Promise.pm index 9bea924..a2ccabb 100644 --- a/App-MHFS/lib/MHFS/Promise.pm +++ b/App-MHFS/lib/MHFS/Promise.pm @@ -103,4 +103,23 @@ sub then { return $promise; } +# static method +sub try { + my $evp = shift @_; + my $func = shift @_; + my $promise; + try { + my $res = $func->(@_); + if (ref($res) eq __PACKAGE__) { + return $res; + } + $promise = MHFS::Promise->_new($evp); + $promise->{fulfill}->($res); + } catch ($e) { + $promise = MHFS::Promise->_new($evp); + $promise->{reject}->($e); + } + $promise +} + 1; diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm index 4ce656e..d4b4a5b 100644 --- a/App-MHFS/lib/MHFS/TMDBClient.pm +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -114,11 +114,13 @@ sub get_image { # returns a promise to path sub get_image_from_metadata { my ($self, $metadata, $image_type, $destdir, $save_base) = @_; - exists $metadata->{$image_type} or die "$image_type does not exist in metadata"; - my $imagepartial = $metadata->{$image_type}; - my ($ext) = $imagepartial =~ /(\.[^\.]+)$/ or die "file extension not found in $imagepartial"; - make_path($destdir); - $self->get_image("original$imagepartial", "$destdir/$save_base$ext") + MHFS::Promise::try($self->{server}{evp}, sub { + exists $metadata->{$image_type} or die "$image_type does not exist in metadata"; + my $imagepartial = $metadata->{$image_type}; + my ($ext) = $imagepartial =~ /(\.[^\.]+)$/ or die "file extension not found in $imagepartial"; + make_path($destdir); + $self->get_image("original$imagepartial", "$destdir/$save_base$ext") + }) } 1; From 725b7f55cdb860abdaaf5a53b20eb29bd8115f49 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Wed, 22 Oct 2025 23:41:03 -0400 Subject: [PATCH 12/15] switch the rest of MHFS::TMDBClient and MHFS::Kodi::TVShows to use and take advantage of MHFS::Promise::try --- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 13 ++----------- App-MHFS/lib/MHFS/TMDBClient.pm | 23 +++++++++-------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index ffe0095..d03e4df 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -173,11 +173,7 @@ sub _fetch_metadata { try { my $plot = $self->get_plot($medianame, $season, $episode); say "fastest path"; - my $result = {text => $plot}; - return MHFS::Promise->new($self->{server}{evp}, sub { - my ($resolve, $reject) = @_; - $resolve->($result); - }); + return {text => $plot}; } catch ($e) {} } my $b_metadir = $self->{tvmeta} . '/' . encode_utf8($medianame) . (defined $season ? '/'.encode_utf8($season). (defined $episode ? '/'.encode_utf8($episode) : '') : ''); @@ -187,12 +183,7 @@ sub _fetch_metadata { if (exists $acceptable{$metadatatype}) { foreach my $totry (@{$acceptable{$metadatatype}}) { my $path = $b_metadir.'/'.$metadatatype.".$totry"; - if (-f $path) { - return MHFS::Promise->new($self->{server}{evp}, sub { - my ($resolve, $reject) = @_; - $resolve->({file => $path}); - }); - } + return {file => $path} if (-f $path); } } } diff --git a/App-MHFS/lib/MHFS/TMDBClient.pm b/App-MHFS/lib/MHFS/TMDBClient.pm index d4b4a5b..7bb81fb 100644 --- a/App-MHFS/lib/MHFS/TMDBClient.pm +++ b/App-MHFS/lib/MHFS/TMDBClient.pm @@ -39,8 +39,7 @@ sub _curl_Promise { sub _TMDB_api_promise { my ($self, $route, $qs) = @_; - MHFS::Promise->new($self->{server}{evp}, sub { - my ($resolve, $reject) = @_; + MHFS::Promise::try($self->{server}{evp}, sub { my $url = 'https://api.themoviedb.org/3/' . $route; $url .= '?api_key=' . $self->{api_key} . '&'; if($qs){ @@ -58,10 +57,10 @@ sub _TMDB_api_promise { } } chop $url; - $resolve->(_curl_Promise($self->{server}, ['-f', encode_utf8($url)])->then(sub { + _curl_Promise($self->{server}, ['-f', encode_utf8($url)])->then(sub { $_[0]->{exit_code} == 0 or die "curl to $url failed with " . $_[0]->{exit_code}; decode_json($_[0]->{stdout}) - })); + }) }) } @@ -87,16 +86,12 @@ sub get_tv_season { sub _get_config { my ($self) = @_; - MHFS::Promise->new($self->{server}{evp}, sub { - my ($resolve, $reject) = @_; - if(! defined $self->{tmdbconfig}) { - $resolve->($self->_TMDB_api_promise('configuration')->then( sub { - $self->{tmdbconfig} = $_[0]; - return $_[0]; - })); - } else { - $resolve->($self->{tmdbconfig}); - } + MHFS::Promise::try($self->{server}{evp}, sub { + return $self->{tmdbconfig} if exists $self->{tmdbconfig}; + $self->_TMDB_api_promise('configuration')->then( sub { + $self->{tmdbconfig} = $_[0]; + $_[0] + }) }) } From 7efb062ac3d276bf8c523a7c3dd3922e219794da Mon Sep 17 00:00:00 2001 From: G4Vi Date: Thu, 23 Oct 2025 00:43:20 -0400 Subject: [PATCH 13/15] refactor: switch rest of route_metadata to use MHFS::TMDBClient --- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 159 +++++-------------------------- 1 file changed, 23 insertions(+), 136 deletions(-) diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index c9d5ad0..8151251 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -6,24 +6,23 @@ use File::Basename qw(basename); use Cwd qw(abs_path getcwd); use URI::Escape qw(uri_escape); use Encode qw(decode encode_utf8); +use Feature::Compat::Try; use File::Path qw(make_path); use Data::Dumper qw(Dumper); use Scalar::Util qw(weaken); use MIME::Base64 qw(encode_base64url decode_base64url); use Devel::Peek qw(Dump); -use MHFS::Kodi::TVShows; use MHFS::Kodi::Movie; use MHFS::Kodi::MovieEdition; use MHFS::Kodi::MovieEditions; use MHFS::Kodi::MoviePart; use MHFS::Kodi::Movies; use MHFS::Kodi::MovieSubtitle; -use MHFS::Kodi::Season; +use MHFS::Kodi::TVShows; use MHFS::Process; use MHFS::Promise; use MHFS::TMDBClient; use MHFS::Util qw(base64url_to_str str_to_base64url uri_escape_path_utf8 read_text_file_lossy write_text_file_lossy decode_utf_8 escape_html_noquote fold_case write_file read_file); -use Feature::Compat::Try; BEGIN { if( ! (eval "use JSON; 1")) { eval "use JSON::PP; 1" or die "No implementation of JSON available"; @@ -530,73 +529,6 @@ sub _zip_Promise { }); } -sub _curl { - my ($server, $params, $cb) = @_; - my $process; - my @cmd = ('curl', @$params); - print "$_ " foreach @cmd; - print "\n"; - $process = MHFS::Process->new_io_process($server->{evp}, \@cmd, sub { - my ($output, $error) = @_; - $cb->($output); - }); - - if(! $process) { - $cb->(undef); - } - - return $process; -} - -sub _TMDB_api { - my ($server, $route, $qs, $cb) = @_; - my $url = 'https://api.themoviedb.org/3/' . $route; - $url .= '?api_key=' . $server->{settings}{TMDB} . '&'; - if($qs){ - foreach my $key (keys %{$qs}) { - my @values; - if(ref($qs->{$key}) ne 'ARRAY') { - push @values, $qs->{$key}; - } - else { - @values = @{$qs->{$key}}; - } - foreach my $value (@values) { - $url .= uri_escape($key).'='.uri_escape($value) . '&'; - } - } - } - chop $url; - return _curl($server, [encode_utf8($url)], sub { - $cb->(decode_json($_[0])); - }); -} - -sub _TMDB_api_promise { - my ($server, $route, $qs) = @_; - return MHFS::Promise->new($server->{evp}, sub { - my ($resolve, $reject) = @_; - _TMDB_api($server, $route, $qs, sub { - $resolve->($_[0]); - }); - }); -} - -sub _DownloadFile { - my ($server, $url, $dest, $cb) = @_; - return _curl($server, ['-k', $url, '-o', $dest], $cb); -} - -sub _DownloadFile_promise { - my ($server, $url, $dest) = @_; - return MHFS::Promise->new($server->{evp}, sub { - my ($resolve, $reject) = @_; - _DownloadFile($server, $url, $dest, sub { - $resolve->(); - }); - }); -} - sub DirectoryRoute { my ($path_without_end_slash, $cb) = @_; return ([ @@ -652,32 +584,16 @@ sub route_metadata { }); return; } - my $tvshows = $self->_get_tvshows_instance() if $mediatype eq 'tv'; - # tv fastest path, grab from the db - if ($mediatype eq 'tv' && $metadatatype eq 'plot') { - try { - my $plot = $tvshows->get_plot($medianame, $season, $episode); - say "fastest path"; - $request->SendText('text/plain; charset=utf-8', $plot); - return; - } catch ($e) {} - } - my %allmediaparams = ( 'movies' => { - 'meta' => $self->{moviemeta}, - 'search' => 'movie', - }, 'tv' => { - 'meta' => $self->{tvmeta}, - 'search' => 'tv' - }); - my $params = $allmediaparams{$mediatype}; - my $b_metadir = $params->{meta} . '/' . encode_utf8($medianame) . (defined $season ? '/'.encode_utf8($season). (defined $episode ? '/'.encode_utf8($episode) : '') : ''); + # TODO movies fastest path, grab from db + my $b_metadir = $self->{moviemeta} . '/' . encode_utf8($medianame); # fast path, check disk - if (($mediatype ne 'tv' || $metadatatype ne 'plot') && -d $b_metadir) { + if ((1 || $metadatatype ne 'plot') && -d $b_metadir) { my %acceptable = ( 'thumb' => ['png', 'jpg'], 'fanart' => ['png', 'jpg'], 'plot' => ['txt']); if(exists $acceptable{$metadatatype}) { foreach my $totry (@{$acceptable{$metadatatype}}) { my $path = $b_metadir.'/'.$metadatatype.".$totry"; if(-f $path) { + say "disk path"; $request->SendLocalFile($path); return; } @@ -685,64 +601,35 @@ sub route_metadata { } } # slow path, download it - $request->{client}{server}{settings}{TMDB} or do { + my $tmdb; + try { + $tmdb = $self->_get_tmdb_instance(); + } catch ($e) { $request->Send404; return; - }; + } # find the movie or tv show my $searchname = $medianame; $searchname =~ s/\s\(\d\d\d\d\)// if($mediatype eq 'movies'); say "searchname $searchname"; weaken($request); - _TMDB_api_promise($request->{client}{server}, 'search/'.$params->{search}, {'query' => $searchname})->then(sub { + $tmdb->search('movie', {'query' => $searchname})->then(sub { my $json = $_[0]->{results}[0]; $json or die "Failed to find item"; - $season // return $json; - # find the season and then the episode if applicable - my $showid = $json->{id} // die "showid not available"; - _TMDB_api_promise($request->{client}{server}, "tv/$showid/season/$season")->then(sub { - $tvshows->insert_season_metadata($medianame, $season, $_[0], $metadatatype eq 'plot'); - $episode // return $_[0]; - MHFS::Kodi::Season::_get_season_episode($_[0], $episode) - }) - })->then(sub { - # get the metadata - if (! defined $season) { - if ($mediatype eq 'tv') { - $tvshows->insert_show_plot($medianame, $_[0], $metadatatype eq 'plot'); - } elsif ($metadatatype eq 'plot' || ! -f "$b_metadir/plot.txt") { - make_path($b_metadir); - try { write_text_file_lossy("$b_metadir/plot.txt", $_[0]->{overview}) } - catch ($e) { say "wierd, creating file failed?"; } - } + if ($metadatatype eq 'plot' || ! -f "$b_metadir/plot.txt") { + make_path($b_metadir); + try { write_text_file_lossy("$b_metadir/plot.txt", $json->{overview}) } + catch ($e) { say "wierd, creating file failed?"; } } - if($metadatatype eq 'plot') { - $request->SendText('text/plain; charset=utf-8', $_[0]->{overview}); + if ($metadatatype eq 'plot') { + $request->SendText('text/plain; charset=utf-8', $json->{overview}); return; } - # thumb or fanart - my $imagepartial = ($metadatatype eq 'thumb') ? (! defined $episode ? $_[0]->{poster_path} : $_[0]->{still_path}) : $_[0]->{backdrop_path}; - if (!$imagepartial || $imagepartial !~ /(\.[^\.]+)$/) { - die 'path not matched '.$imagepartial; - } - my $ext = $1; - make_path($b_metadir); - return MHFS::Promise->new($request->{client}{server}{evp}, sub { - my ($resolve, $reject) = @_; - if(! defined $self->{tmdbconfig}) { - $resolve->(_TMDB_api_promise($request->{client}{server}, 'configuration')->then( sub { - $self->{tmdbconfig} = $_[0]; - return $_[0]; - })); - } else { - $resolve->(); - } - })->then( sub { - return _DownloadFile_promise($request->{client}{server}, $self->{tmdbconfig}{images}{secure_base_url}.'original'.$imagepartial, "$b_metadir/$metadatatype$ext")->then(sub { - $request->SendLocalFile("$b_metadir/$metadatatype$ext"); - return; - }); - }); + my $image_type = ($metadatatype eq 'thumb') ? 'poster_path' : 'backdrop_path'; + $tmdb->get_image_from_metadata($json, $image_type, $b_metadir, $metadatatype)->then(sub { + $request->SendLocalFile($_[0]); + return; + }) })->then(undef, sub { print $_[0]; $request->Send404; From 8aa0beb138e80f0139f7d8ae47cc045e59b38ae7 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Thu, 23 Oct 2025 02:01:32 -0400 Subject: [PATCH 14/15] refactor: move building and searching movie library to Movies.pm --- App-MHFS/lib/MHFS/Kodi/Movies.pm | 240 +++++++++++++++++++++++++++- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 258 +++---------------------------- 2 files changed, 257 insertions(+), 241 deletions(-) diff --git a/App-MHFS/lib/MHFS/Kodi/Movies.pm b/App-MHFS/lib/MHFS/Kodi/Movies.pm index 11e710d..4bb32b8 100644 --- a/App-MHFS/lib/MHFS/Kodi/Movies.pm +++ b/App-MHFS/lib/MHFS/Kodi/Movies.pm @@ -1,9 +1,247 @@ package MHFS::Kodi::Movies v0.7.0; use 5.014; use strict; use warnings; +use Encode qw(encode_utf8); +use Feature::Compat::Try; use File::Basename qw(basename); -use MHFS::Kodi::Util qw(html_list_item); use MHFS::Kodi::Movie; +use MHFS::Kodi::MovieEdition; +use MHFS::Kodi::MovieEditions; +use MHFS::Kodi::MoviePart; +use MHFS::Kodi::MovieSubtitle; +use MHFS::Kodi::Util qw(html_list_item); +use MHFS::Util qw(decode_utf_8 read_text_file_lossy); + +sub _readsubdir{ + my ($subtitles, $source, $b_path) = @_; + opendir( my $dh, $b_path ) or return; + while(my $b_filename = readdir($dh)) { + next if(($b_filename eq '.') || ($b_filename eq '..')); + my $filename = do { + try { decode_utf_8($b_filename) } + catch($e) { + warn "$b_filename is not, UTF-8, skipping"; + next; + } + }; + my $b_nextpath = "$b_path/$b_filename"; + my $nextsource = "$source/$filename"; + if(-f $b_nextpath && $filename =~ /\.(?:srt|sub|idx)$/) { + push @$subtitles, $nextsource; + next; + } elsif (-d _) { + _readsubdir($subtitles, $nextsource, $b_nextpath); + } + } +} + +sub _readmoviedir { + my ($self, $movies, $source, $b_moviedir) = @_; + opendir(my $dh, $b_moviedir ) or do { + warn "Error in opening dir $b_moviedir\n"; + return; + }; + while(my $b_edition = readdir($dh)) { + next if(($b_edition eq '.') || ($b_edition eq '..')); + my $edition = do { + try { decode_utf_8($b_edition) } + catch($e) { + warn "$b_edition is not, UTF-8, skipping"; + next; + } + }; + my $b_path = "$b_moviedir/$b_edition"; + # recurse on collections + if ($edition =~ /(?:Duology|Trilogy|Quadrilogy)/) { + next if ($edition =~ /\.nfo$/); + $self->_readmoviedir($movies, "$source/$edition", $b_path); + next; + } + -s $b_path or next; + my $isdir = -d _; + $isdir || -f _ or next; + $isdir ||= 0; + my %edition; + if (!$isdir) { + if ($edition !~ /\.(?:avi|mkv|mp4|m4v)$/) { + warn "Skipping $edition, not a movie file" if ($edition !~ /\.(?:txt)$/); + next; + } + $edition{''} = {}; + } else { + my @videos; + my @subtitles; + my @subtitledirs; + opendir(my $dh, $b_path) or do { + warn 'failed to open dir'; + next; + }; + while(my $b_editionitem = readdir($dh)) { + next if(($b_editionitem eq '.') || ($b_editionitem eq '..')); + my $editionitem = do { + try { decode_utf_8($b_editionitem) } + catch($e) { + warn "$b_editionitem is not, UTF-8, skipping"; + next; + } + }; + my $type; + if ($editionitem =~ /\.(?:avi|mkv|mp4|m4v)$/) { + $type = 'video' if ($editionitem !~ /sample(?:\-[a-z]+)?\.(?:avi|mkv|mp4|m4v)$/); + } elsif ($editionitem =~ /\.(?:srt|sub|idx)$/) { + $type = 'subtitle'; + } elsif ($editionitem =~ /^Subs$/i) { + $type = 'subtitledir'; + } + $type or next; + if (-f "$b_path/$b_editionitem") { + push @videos, $editionitem if($type eq 'video'); + push @subtitles, $editionitem if($type eq 'subtitle'); + } elsif (-d _ && $type eq 'subtitledir') { + push @subtitledirs, $editionitem; + } + } + closedir($dh); + if (!@videos) { + warn "not adding edition $edition, no videos found"; + next; + } + foreach my $subdir (@subtitledirs) { + _readsubdir(\@subtitles, $subdir, "$b_path/$subdir"); + } + foreach my $videofile (@videos) { + my ($withoutext) = $videofile =~ /^(.+)\.[^\.]+$/; + my %relevantsubs; + for my $i (reverse 0 .. $#subtitles) { + if (basename($subtitles[$i]) =~ /^\Q$withoutext\E/i) { + $relevantsubs{splice(@subtitles, $i, 1)} = undef; + } + } + $edition{"/$videofile"} = scalar %relevantsubs ? {subs => \%relevantsubs} : {}; + } + if(@subtitles) { + warn "$edition: unmatched subtitle $_" foreach @subtitles; + } + } + my $showname; + my $withoutyear; + my $year; + if($edition =~ /^(.+)[\.\s]+\(?(\d{4})([^p]|$)/) { + $showname = "$1 ($2)"; + $withoutyear = $1; + $year = $2; + $withoutyear =~ s/\./ /g; + } + elsif ($edition =~ /(.+)\s?\[(\d{4})\]/) { + $showname = "$1 ($2)"; + $withoutyear = $1; + $year = $2; + $withoutyear =~ s/\./ /g; + } + elsif($edition =~ /^(.+)[\.\s](?i:DVDRip)[\.\s]./) { + $showname = $1; + } + elsif($edition =~ /^(.+)[\.\s](?:DVD|RERIP|BRrip)/) { + $showname = $1; + } + elsif($edition =~ /^(.+)\s\(PSP.+\)/) { + $showname = $1; + } + elsif($edition =~ /^(.+)\.VHS/) { + $showname = $1; + } + elsif($edition =~ /^(.+)[\.\s]+\d{3,4}p\./) { + $showname = $1; + } + elsif($edition =~ /^(.+)\.[a-zA-Z\d]{3,4}$/) { + $showname = $1; + } + else{ + $showname = $edition; + } + $showname =~ s/\./ /g; + if(! $movies->{$showname}) { + my %diritem; + if(defined $year) { + $diritem{name} = $withoutyear; + $diritem{year} = $year; + } + my $b_showname = encode_utf8($showname); + my $plot = $self->{moviemeta}."/$b_showname/plot.txt"; + try { $diritem{plot} = read_text_file_lossy($plot); } + catch($e) {} + $movies->{$showname} = \%diritem; + } + $movies->{$showname}{editions}{"$source/$edition"} = \%edition; + } + closedir($dh); +} + +sub build_movie_library { + my ($self) = @_; + my $sources = $self->{sources}; + my %movies; + foreach my $source (@$sources) { + if ($self->{server}{settings}{SOURCES}{$source}{type} ne 'local') { + warn "skipping source $source, only local implemented"; + next; + } + my $b_moviedir = $self->{server}{settings}{SOURCES}{$source}{folder}; + $self->_readmoviedir(\%movies, $source, $b_moviedir); + } + $self->{movies} = \%movies; +} + +sub new { + my ($name, $server, $moviemeta, $tmdb_client) = @_; + my %optional = ( + ($tmdb_client ? (tmdb => $tmdb_client) : ()), + ); + my $self = bless {server => $server, sources => $server->{settings}{MEDIASOURCES}{movies}, moviemeta => $moviemeta, %optional}, $name; + $self->build_movie_library(); + $self +} + +# dies on not found/error +sub get_movie_item { + my ($self, $movieid, $source, $editionname, $partname, $subfile) = @_; + my $movies = $self->{movies}; + unless(exists $movies->{$movieid}) { + die "movie not found"; + } + $movies = $movies->{$movieid}; + if (!$source) { + return bless {movie => $movies}, 'MHFS::Kodi::Movie'; + } + $movies = $movies->{editions}; + if(!$editionname) { + my %editions = map { $_ =~ /^$source/ ? ($_ => $movies->{$_}) : () } keys %$movies; + return bless {editions => \%editions}, 'MHFS::Kodi::MovieEditions'; + } + unless(exists $movies->{"$source/$editionname"}) { + die "movie source not found"; + } + $movies = $movies->{"$source/$editionname"}; + unless(defined $partname) { + return bless {source => $source, editionname => $editionname, edition => $movies}, 'MHFS::Kodi::MovieEdition'; + } + unless(exists $movies->{$partname}) { + die "movie part not found"; + } + my $b_moviedir = $self->{server}{settings}{SOURCES}{$source}{folder}; + my $b_editionname = encode_utf8($editionname); + my $b_editiondir = "$b_moviedir/$b_editionname"; + $movies = $movies->{$partname}; + if (!$subfile) { + my $b_partname = encode_utf8($partname); + return bless {b_path => "$b_editiondir$b_partname", editionname => $editionname, partname => $partname, part => $movies}, 'MHFS::Kodi::MoviePart'; + } + unless(exists $movies->{subs} && exists $movies->{subs}{$subfile}) { + die "subtitle file not found"; + } + my $b_subfile = encode_utf8($subfile); + return bless {b_path => "$b_editiondir/$b_subfile", subtitle => $subfile}, 'MHFS::Kodi::MovieSubtitle'; +} sub Format { my ($moovies) = @_; diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index 8151251..d3dfbbd 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -2,27 +2,16 @@ package MHFS::Plugin::Kodi v0.7.0; use 5.014; use strict; use warnings; use feature 'say'; -use File::Basename qw(basename); -use Cwd qw(abs_path getcwd); -use URI::Escape qw(uri_escape); -use Encode qw(decode encode_utf8); +use Encode qw(encode_utf8); use Feature::Compat::Try; use File::Path qw(make_path); -use Data::Dumper qw(Dumper); use Scalar::Util qw(weaken); -use MIME::Base64 qw(encode_base64url decode_base64url); -use Devel::Peek qw(Dump); -use MHFS::Kodi::Movie; -use MHFS::Kodi::MovieEdition; -use MHFS::Kodi::MovieEditions; -use MHFS::Kodi::MoviePart; use MHFS::Kodi::Movies; -use MHFS::Kodi::MovieSubtitle; use MHFS::Kodi::TVShows; use MHFS::Process; use MHFS::Promise; use MHFS::TMDBClient; -use MHFS::Util qw(base64url_to_str str_to_base64url uri_escape_path_utf8 read_text_file_lossy write_text_file_lossy decode_utf_8 escape_html_noquote fold_case write_file read_file); +use MHFS::Util qw(base64url_to_str write_text_file_lossy decode_utf_8 fold_case); BEGIN { if( ! (eval "use JSON; 1")) { eval "use JSON::PP; 1" or die "No implementation of JSON available"; @@ -50,6 +39,19 @@ sub _get_tvshows_instance { $self->{tvshows} } +sub _get_movies_instance { + my ($self, $force_reload) = @_; + my $tmdb; + try {$tmdb = $self->_get_tmdb_instance()} + catch ($e){print $e} + if (! exists $self->{movies}) { + $self->{movies} = MHFS::Kodi::Movies->new($self->{server}, $self->{tvmeta}, $tmdb); + return $self->{movies}; + } + $self->{movies}->build_movie_library() if $force_reload; + $self->{movies} +} + # format tv library for kodi http sub route_tv { my ($self, $request, $sources, $kodidir) = @_; @@ -106,225 +108,6 @@ sub route_tv { } } -sub readsubdir{ - my ($subtitles, $source, $b_path) = @_; - opendir( my $dh, $b_path ) or return; - while(my $b_filename = readdir($dh)) { - next if(($b_filename eq '.') || ($b_filename eq '..')); - my $filename = do { - try { decode_utf_8($b_filename) } - catch($e) { - warn "$b_filename is not, UTF-8, skipping"; - next; - } - }; - my $b_nextpath = "$b_path/$b_filename"; - my $nextsource = "$source/$filename"; - if(-f $b_nextpath && $filename =~ /\.(?:srt|sub|idx)$/) { - push @$subtitles, $nextsource; - next; - } elsif (-d _) { - readsubdir($subtitles, $nextsource, $b_nextpath); - } - } -} - -sub readmoviedir { - my ($self, $movies, $source, $b_moviedir) = @_; - opendir(my $dh, $b_moviedir ) or do { - warn "Error in opening dir $b_moviedir\n"; - return; - }; - while(my $b_edition = readdir($dh)) { - next if(($b_edition eq '.') || ($b_edition eq '..')); - my $edition = do { - try { decode_utf_8($b_edition) } - catch($e) { - warn "$b_edition is not, UTF-8, skipping"; - next; - } - }; - my $b_path = "$b_moviedir/$b_edition"; - # recurse on collections - if ($edition =~ /(?:Duology|Trilogy|Quadrilogy)/) { - next if ($edition =~ /\.nfo$/); - $self->readmoviedir($movies, "$source/$edition", $b_path); - next; - } - -s $b_path or next; - my $isdir = -d _; - $isdir || -f _ or next; - $isdir ||= 0; - my %edition; - if (!$isdir) { - if ($edition !~ /\.(?:avi|mkv|mp4|m4v)$/) { - warn "Skipping $edition, not a movie file" if ($edition !~ /\.(?:txt)$/); - next; - } - $edition{''} = {}; - } else { - my @videos; - my @subtitles; - my @subtitledirs; - opendir(my $dh, $b_path) or do { - warn 'failed to open dir'; - next; - }; - while(my $b_editionitem = readdir($dh)) { - next if(($b_editionitem eq '.') || ($b_editionitem eq '..')); - my $editionitem = do { - try { decode_utf_8($b_editionitem) } - catch($e) { - warn "$b_editionitem is not, UTF-8, skipping"; - next; - } - }; - my $type; - if ($editionitem =~ /\.(?:avi|mkv|mp4|m4v)$/) { - $type = 'video' if ($editionitem !~ /sample(?:\-[a-z]+)?\.(?:avi|mkv|mp4|m4v)$/); - } elsif ($editionitem =~ /\.(?:srt|sub|idx)$/) { - $type = 'subtitle'; - } elsif ($editionitem =~ /^Subs$/i) { - $type = 'subtitledir'; - } - $type or next; - if (-f "$b_path/$b_editionitem") { - push @videos, $editionitem if($type eq 'video'); - push @subtitles, $editionitem if($type eq 'subtitle'); - } elsif (-d _ && $type eq 'subtitledir') { - push @subtitledirs, $editionitem; - } - } - closedir($dh); - if (!@videos) { - warn "not adding edition $edition, no videos found"; - next; - } - foreach my $subdir (@subtitledirs) { - readsubdir(\@subtitles, $subdir, "$b_path/$subdir"); - } - foreach my $videofile (@videos) { - my ($withoutext) = $videofile =~ /^(.+)\.[^\.]+$/; - my %relevantsubs; - for my $i (reverse 0 .. $#subtitles) { - if (basename($subtitles[$i]) =~ /^\Q$withoutext\E/i) { - $relevantsubs{splice(@subtitles, $i, 1)} = undef; - } - } - $edition{"/$videofile"} = scalar %relevantsubs ? {subs => \%relevantsubs} : {}; - } - if(@subtitles) { - warn "$edition: unmatched subtitle $_" foreach @subtitles; - } - } - my $showname; - my $withoutyear; - my $year; - if($edition =~ /^(.+)[\.\s]+\(?(\d{4})([^p]|$)/) { - $showname = "$1 ($2)"; - $withoutyear = $1; - $year = $2; - $withoutyear =~ s/\./ /g; - } - elsif ($edition =~ /(.+)\s?\[(\d{4})\]/) { - $showname = "$1 ($2)"; - $withoutyear = $1; - $year = $2; - $withoutyear =~ s/\./ /g; - } - elsif($edition =~ /^(.+)[\.\s](?i:DVDRip)[\.\s]./) { - $showname = $1; - } - elsif($edition =~ /^(.+)[\.\s](?:DVD|RERIP|BRrip)/) { - $showname = $1; - } - elsif($edition =~ /^(.+)\s\(PSP.+\)/) { - $showname = $1; - } - elsif($edition =~ /^(.+)\.VHS/) { - $showname = $1; - } - elsif($edition =~ /^(.+)[\.\s]+\d{3,4}p\./) { - $showname = $1; - } - elsif($edition =~ /^(.+)\.[a-zA-Z\d]{3,4}$/) { - $showname = $1; - } - else{ - $showname = $edition; - } - $showname =~ s/\./ /g; - if(! $movies->{$showname}) { - my %diritem; - if(defined $year) { - $diritem{name} = $withoutyear; - $diritem{year} = $year; - } - my $b_showname = encode_utf8($showname); - my $plot = $self->{moviemeta}."/$b_showname/plot.txt"; - try { $diritem{plot} = read_text_file_lossy($plot); } - catch($e) {} - $movies->{$showname} = \%diritem; - } - $movies->{$showname}{editions}{"$source/$edition"} = \%edition; - } - closedir($dh); -} - -sub _build_movie_library { - my ($self, $sources) = @_; - my %movies; - foreach my $source (@$sources) { - if ($self->{server}{settings}{SOURCES}{$source}{type} ne 'local') { - warn "skipping source $source, only local implemented"; - next; - } - my $b_moviedir = $self->{server}{settings}{SOURCES}{$source}{folder}; - $self->readmoviedir(\%movies, $source, $b_moviedir); - } - \%movies -} - -# dies on not found/error -sub _search_movie_library { - my ($self, $movies, $movieid, $source, $editionname, $partname, $subfile) = @_; - unless(exists $movies->{$movieid}) { - die "movie not found"; - } - $movies = $movies->{$movieid}; - if (!$source) { - return bless {movie => $movies}, 'MHFS::Kodi::Movie'; - } - $movies = $movies->{editions}; - if(!$editionname) { - my %editions = map { $_ =~ /^$source/ ? ($_ => $movies->{$_}) : () } keys %$movies; - return bless {editions => \%editions}, 'MHFS::Kodi::MovieEditions'; - } - unless(exists $movies->{"$source/$editionname"}) { - die "movie source not found"; - } - $movies = $movies->{"$source/$editionname"}; - unless(defined $partname) { - return bless {source => $source, editionname => $editionname, edition => $movies}, 'MHFS::Kodi::MovieEdition'; - } - unless(exists $movies->{$partname}) { - die "movie part not found"; - } - my $b_moviedir = $self->{server}{settings}{SOURCES}{$source}{folder}; - my $b_editionname = encode_utf8($editionname); - my $b_editiondir = "$b_moviedir/$b_editionname"; - $movies = $movies->{$partname}; - if (!$subfile) { - my $b_partname = encode_utf8($partname); - return bless {b_path => "$b_editiondir$b_partname", editionname => $editionname, partname => $partname, part => $movies}, 'MHFS::Kodi::MoviePart'; - } - unless(exists $movies->{subs} && exists $movies->{subs}{$subfile}) { - die "subtitle file not found"; - } - my $b_subfile = encode_utf8($subfile); - return bless {b_path => "$b_editiondir/$b_subfile", subtitle => $subfile}, 'MHFS::Kodi::MovieSubtitle'; -} - # format movies library for kodi http sub route_movies { my ($self, $request, $sources, $kodidir) = @_; @@ -336,12 +119,7 @@ sub route_movies { return; } }; - # build the movie library - if(! exists $self->{movies} || $request_path eq $kodidir) { - $self->{movies} = $self->_build_movie_library($sources); - } - my $movies = $self->{movies}; - # find the movie item + my $movies = $self->_get_movies_instance($request_path eq $kodidir); my $movieitem; if($request_path ne $kodidir) { my $fullmoviepath = substr($request_path, length($kodidir)+1); @@ -385,7 +163,7 @@ sub route_movies { } } } - $movieitem = $self->_search_movie_library($movies, $movieid, $source, $editionname, $partname, $subfile); + $movieitem = $movies->get_movie_item($movieid, $source, $editionname, $partname, $subfile); } catch ($e) { $request->Send404; return; @@ -400,7 +178,7 @@ sub route_movies { return; } } else { - $movieitem = bless {movies => $movies}, 'MHFS::Kodi::Movies'; + $movieitem = $movies; } # render if(exists $request->{qs}{fmt} && $request->{qs}{fmt} eq 'html') { From 1b5a2c51af10ab4426b380a10898ece128a063c3 Mon Sep 17 00:00:00 2001 From: G4Vi Date: Fri, 24 Oct 2025 01:51:06 -0400 Subject: [PATCH 15/15] kodi movie metadata: fold_case movieids, fix metadata dir path, move fetching metadata into Movies.pm --- .../lib/MHFS/HTTP/Server/Client/Request.pm | 6 ++ App-MHFS/lib/MHFS/Kodi/Movies.pm | 74 +++++++++++++++++- App-MHFS/lib/MHFS/Kodi/TVShows.pm | 2 +- App-MHFS/lib/MHFS/Plugin/Kodi.pm | 77 ++++--------------- 4 files changed, 96 insertions(+), 63 deletions(-) diff --git a/App-MHFS/lib/MHFS/HTTP/Server/Client/Request.pm b/App-MHFS/lib/MHFS/HTTP/Server/Client/Request.pm index 94b954f..f4cf95a 100644 --- a/App-MHFS/lib/MHFS/HTTP/Server/Client/Request.pm +++ b/App-MHFS/lib/MHFS/HTTP/Server/Client/Request.pm @@ -404,6 +404,12 @@ sub Send416 { $self->SendHTML('', {'code' => 416}); } +sub Send500 { + my ($self) = @_; + my $msg = "500 Internal Server Error"; + $self->SendHTML($msg, {'code' => 500}); +} + sub Send503 { my ($self) = @_; $self->{'outheaders'}{'Retry-After'} = 5; diff --git a/App-MHFS/lib/MHFS/Kodi/Movies.pm b/App-MHFS/lib/MHFS/Kodi/Movies.pm index 4bb32b8..04b4381 100644 --- a/App-MHFS/lib/MHFS/Kodi/Movies.pm +++ b/App-MHFS/lib/MHFS/Kodi/Movies.pm @@ -3,6 +3,7 @@ use 5.014; use strict; use warnings; use Encode qw(encode_utf8); use Feature::Compat::Try; +use File::Path qw(make_path); use File::Basename qw(basename); use MHFS::Kodi::Movie; use MHFS::Kodi::MovieEdition; @@ -10,7 +11,7 @@ use MHFS::Kodi::MovieEditions; use MHFS::Kodi::MoviePart; use MHFS::Kodi::MovieSubtitle; use MHFS::Kodi::Util qw(html_list_item); -use MHFS::Util qw(decode_utf_8 read_text_file_lossy); +use MHFS::Util qw(decode_utf_8 read_text_file_lossy fold_case write_text_file_lossy); sub _readsubdir{ my ($subtitles, $source, $b_path) = @_; @@ -160,6 +161,7 @@ sub _readmoviedir { $showname = $edition; } $showname =~ s/\./ /g; + $showname = fold_case($showname); if(! $movies->{$showname}) { my %diritem; if(defined $year) { @@ -243,6 +245,76 @@ sub get_movie_item { return bless {b_path => "$b_editiondir/$b_subfile", subtitle => $subfile}, 'MHFS::Kodi::MovieSubtitle'; } +sub get_plot { + my ($self, $movieid) = @_; + my $item = $self->{movies}; + exists $item->{$movieid} or die "movieid $movieid does not exist"; + $item = $item->{$movieid}; + exists $item->{plot} or die "movieid $movieid does not have plot yet"; + $item->{plot} +} + +# IF NOT EXISTS unless $force_update is true +sub insert_movie_plot { + my ($self, $movieid, $metadata, $force_update) = @_; + exists $metadata->{overview} or die "metadata does not have plot"; + my $plot = $metadata->{overview}; + my $item = $self->{movies}; + exists $item->{$movieid} or die "movieid $movieid does not exist"; + $item = $item->{$movieid}; + return if (exists $item->{plot} && !$force_update); + my $b_metadir = $self->{moviemeta} . '/' . encode_utf8($movieid); + make_path($b_metadir); + write_text_file_lossy("$b_metadir/plot.txt", $plot); + $item->{plot} = $plot; +} + +sub _fetch_metadata { + my ($self, $metadatatype, $medianame) = @_; + # fastest path, grab from the db + if ($metadatatype eq 'plot') { + try { + my $plot = $self->get_plot($medianame); + say "fastest path"; + return {text => $plot}; + } catch ($e) {} + } + my $b_metadir = $self->{moviemeta} . '/' . encode_utf8($medianame); + # fast path, check disk + if ($metadatatype ne 'plot' && -d $b_metadir) { + my %acceptable = ( 'thumb' => ['png', 'jpg'], 'fanart' => ['png', 'jpg']); + if(exists $acceptable{$metadatatype}) { + foreach my $totry (@{$acceptable{$metadatatype}}) { + my $path = $b_metadir.'/'.$metadatatype.".$totry"; + return {file => $path} if (-f $path); + } + } + } + # slow path, download it + exists $self->{tmdb} or die "cannot load metadata without tmdb"; + my $tmdb = $self->{tmdb}; + # find the movie + my $searchname = $medianame; + $searchname =~ s/\s\(\d\d\d\d\)//; + say "searchname $searchname"; + $tmdb->search('movie', {'query' => $searchname})->then(sub { + my $json = $_[0]->{results}[0]; + $json or die "Failed to find item"; + $self->insert_movie_plot($medianame, $json, $metadatatype eq 'plot'); + if ($metadatatype eq 'plot') { + return {text => $json->{overview}}; + } + my $image_type = ($metadatatype eq 'thumb') ? 'poster_path' : 'backdrop_path'; + $tmdb->get_image_from_metadata($json, $image_type, $b_metadir, $metadatatype)->then(sub { + {file => $_[0]} + }) + }) +} + +sub fetch_metadata { + MHFS::Promise::try($_[0]->{server}{evp}, \&_fetch_metadata, @_) +} + sub Format { my ($moovies) = @_; my @sortedkeys = sort {basename($a) cmp basename($b)} keys %$moovies; diff --git a/App-MHFS/lib/MHFS/Kodi/TVShows.pm b/App-MHFS/lib/MHFS/Kodi/TVShows.pm index d03e4df..8e58075 100644 --- a/App-MHFS/lib/MHFS/Kodi/TVShows.pm +++ b/App-MHFS/lib/MHFS/Kodi/TVShows.pm @@ -190,7 +190,7 @@ sub _fetch_metadata { # slow path, download it exists $self->{tmdb} or die "cannot load metadata without tmdb"; my $tmdb = $self->{tmdb}; - # find the movie or tv show + # find the tv show, season, or episode my $searchname = $medianame; say "searchname $searchname"; return $tmdb->search('tv', {'query' => $searchname})->then(sub { diff --git a/App-MHFS/lib/MHFS/Plugin/Kodi.pm b/App-MHFS/lib/MHFS/Plugin/Kodi.pm index d3dfbbd..a6a6840 100644 --- a/App-MHFS/lib/MHFS/Plugin/Kodi.pm +++ b/App-MHFS/lib/MHFS/Plugin/Kodi.pm @@ -45,7 +45,7 @@ sub _get_movies_instance { try {$tmdb = $self->_get_tmdb_instance()} catch ($e){print $e} if (! exists $self->{movies}) { - $self->{movies} = MHFS::Kodi::Movies->new($self->{server}, $self->{tvmeta}, $tmdb); + $self->{movies} = MHFS::Kodi::Movies->new($self->{server}, $self->{moviemeta}, $tmdb); return $self->{movies}; } $self->{movies}->build_movie_library() if $force_reload; @@ -344,74 +344,29 @@ sub route_metadata { } $medianame = fold_case($medianame); say "mt $mediatype mmt $metadatatype mn $medianame". (defined $season ? " season $season". (defined $episode ? " episode $episode" : '') : ''); + my $library; if ($mediatype eq 'tv') { - weaken($request); - $self->_get_tvshows_instance()->fetch_metadata($metadatatype, $medianame, $season, $episode)->then(sub { - my ($result) = @_; - if ($result->{file}) { - $request->SendLocalFile($result->{file}); - } elsif ($result->{text}) { - $request->SendText('text/plain; charset=utf-8', $result->{text}); - } else { - die "unknown result type"; - } - })->then(undef, sub { - print $_[0]; - say "fetch_metadata failure"; - $request->Send404; - }); - return; - } - # TODO movies fastest path, grab from db - my $b_metadir = $self->{moviemeta} . '/' . encode_utf8($medianame); - # fast path, check disk - if ((1 || $metadatatype ne 'plot') && -d $b_metadir) { - my %acceptable = ( 'thumb' => ['png', 'jpg'], 'fanart' => ['png', 'jpg'], 'plot' => ['txt']); - if(exists $acceptable{$metadatatype}) { - foreach my $totry (@{$acceptable{$metadatatype}}) { - my $path = $b_metadir.'/'.$metadatatype.".$totry"; - if(-f $path) { - say "disk path"; - $request->SendLocalFile($path); - return; - } - } - } - } - # slow path, download it - my $tmdb; - try { - $tmdb = $self->_get_tmdb_instance(); - } catch ($e) { - $request->Send404; + $library = $self->_get_tvshows_instance(); + } elsif ($mediatype eq 'movies') { + $library = $self->_get_movies_instance(); + } else { + $request->Send500; return; } - # find the movie or tv show - my $searchname = $medianame; - $searchname =~ s/\s\(\d\d\d\d\)// if($mediatype eq 'movies'); - say "searchname $searchname"; weaken($request); - $tmdb->search('movie', {'query' => $searchname})->then(sub { - my $json = $_[0]->{results}[0]; - $json or die "Failed to find item"; - if ($metadatatype eq 'plot' || ! -f "$b_metadir/plot.txt") { - make_path($b_metadir); - try { write_text_file_lossy("$b_metadir/plot.txt", $json->{overview}) } - catch ($e) { say "wierd, creating file failed?"; } + $library->fetch_metadata($metadatatype, $medianame, $season, $episode)->then(sub { + my ($result) = @_; + if ($result->{file}) { + $request->SendLocalFile($result->{file}); + } elsif ($result->{text}) { + $request->SendText('text/plain; charset=utf-8', $result->{text}); + } else { + die "unknown result type"; } - if ($metadatatype eq 'plot') { - $request->SendText('text/plain; charset=utf-8', $json->{overview}); - return; - } - my $image_type = ($metadatatype eq 'thumb') ? 'poster_path' : 'backdrop_path'; - $tmdb->get_image_from_metadata($json, $image_type, $b_metadir, $metadatatype)->then(sub { - $request->SendLocalFile($_[0]); - return; - }) })->then(undef, sub { print $_[0]; + say "fetch_metadata failure"; $request->Send404; - return; }); return; }