App-MHFS
view release on metacpan or search on metacpan
lib/MHFS/Plugin/Kodi.pm view on Meta::CPAN
$tvshows->{$showid}{seasons}{$season}{"$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);
}
# load the season metadata, maybe remove this if we allow querying a show without a season
while (my ($showid, $show) = each %tvshows) {
while (my ($seasonid, $season) = each %{$show->{seasons}}) {
my $meta;
try {
my $bytes = read_file($self->{tvmeta}."/$showid/$seasonid/season.json");
$meta = decode_json($bytes);
} catch($e) {}
$meta or next;
# HACK: modifies each season item as there isn't a season object
foreach my $value (values %$season) {
$value->{plot} = $meta->{overview};
}
}
}
\%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};
my $meta;
try {
my $bytes = read_file($self->{tvmeta}."/$showid/$seasonid/season.json");
$meta = decode_json($bytes);
} catch($e) {}
$source or return bless {season => $seasonitem, id => $seasonid, sourcemap => $sourcemap, ($meta ? (meta => $meta) : ())}, '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}
}
# format tv library for kodi http
sub route_tv {
my ($self, $request, $sources, $kodidir) = @_;
my $request_path = do {
try { decode_utf_8($request->{path}{unsafepath}) }
catch($e) {
warn "$request->{path}{unsafepath} is not, UTF-8, 404";
$request->Send404;
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 $tvitem;
if ($request_path ne $kodidir) {
my $fulltvpath = substr($request_path, length($kodidir)+1);
say "fulltvpath $fulltvpath";
my ($showid, $season, $source, $b64_item, $slurp) = split('/', $fulltvpath, 5);
if ($slurp) {
say "too many parts";
$request->Send400;
return;
}
$showid = fold_case($showid);
$season // do {
say "no season provided";
$request->Send400;
return;
};
try {
$tvitem = $self->_get_tv_item($tvshows, $showid, $season, $source, $b64_item);
} catch($e) {
say "exception $e";
$request->Send404;
return;
}
if (substr($request->{'path'}{'unescapepath'}, -1) ne '/') {
# redirect if we aren't accessing a file
if (!exists $tvitem->{b_path}) {
$request->SendRedirect(301, substr($request->{'path'}{'unescapepath'}, rindex($request->{'path'}{'unescapepath'}, '/')+1).'/');
} else {
$request->SendFile($tvitem->{b_path});
}
return;
}
} else {
$tvitem = bless {tvshows => $tvshows}, 'MHFS::Kodi::TVShows';
}
if(exists $request->{qs}{fmt} && $request->{qs}{fmt} eq 'html') {
my $buf = $tvitem->TO_HTML;
$request->SendHTML($buf);
} else {
my $diritems = $tvitem->TO_JSON;
$request->SendAsJSON($diritems);
}
}
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;
}
lib/MHFS/Plugin/Kodi.pm view on Meta::CPAN
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) = @_;
my $request_path = do {
try { decode_utf_8($request->{path}{unsafepath}) }
catch($e) {
warn "$request->{path}{unsafepath} is not, UTF-8, 404";
$request->Send404;
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 $movieitem;
if($request_path ne $kodidir) {
my $fullmoviepath = substr($request_path, length($kodidir)+1);
say "fullmoviepath $fullmoviepath";
my ($movieid, $source, $b64_editionname, $b64_partname, $b64_subpath, $subname, $slurp) = split('/', $fullmoviepath, 7);
if ($slurp) {
say "too many parts";
$request->Send404;
return;
}
say "movieid $movieid";
my $editionname;
my $partname;
my $subfile;
try {
if ($source) {
say "source $source";
if ($b64_editionname) {
$editionname = base64url_to_str($b64_editionname);
say "editionname $editionname";
if ($b64_partname) {
if (length($b64_partname) < 3) {
warn "$b64_partname has invalid format";
$request->Send404;
return;
}
$b64_partname = substr($b64_partname, 0, -3);
$partname = base64url_to_str($b64_partname);
say "partname $partname";
if ($b64_subpath && $subname) {
if (length($b64_subpath) < 3) {
warn "$b64_subpath has invalid format";
$request->Send404;
return;
}
$b64_subpath = substr($b64_subpath, 0, -3);
my $subpath = base64url_to_str($b64_subpath);
$subfile = "$subpath$subname";
say "subfile $subfile";
}
}
}
}
$movieitem = $self->_search_movie_library($movies, $movieid, $source, $editionname, $partname, $subfile);
} catch ($e) {
$request->Send404;
return;
}
if (substr($request->{'path'}{'unescapepath'}, -1) ne '/') {
# redirect if we aren't accessing a file
if (!exists $movieitem->{b_path}) {
$request->SendRedirect(301, substr($request->{'path'}{'unescapepath'}, rindex($request->{'path'}{'unescapepath'}, '/')+1).'/');
} else {
$request->SendFile($movieitem->{b_path});
}
return;
}
} else {
$movieitem = bless {movies => $movies}, 'MHFS::Kodi::Movies';
}
# render
if(exists $request->{qs}{fmt} && $request->{qs}{fmt} eq 'html') {
my $buf = $movieitem->TO_HTML;
$request->SendHTML($buf);
} else {
my $diritems = $movieitem->TO_JSON;
$request->SendAsJSON($diritems);
}
}
sub route_kodi {
my ($self, $request, $kodidir) = @_;
my $request_path = do {
try { decode_utf_8($request->{path}{unsafepath}) }
catch($e) {
warn "$request->{path}{unsafepath} is not, UTF-8, 404";
$request->Send404;
return;
}
};
my $baseurl = $request->getAbsoluteURL;
my $repo_addon_version = '0.1.0';
my $repo_addon_name = "repository.mhfs-$repo_addon_version.zip";
if ($request_path eq $kodidir) {
my $html = <<"END_HTML";
<style>ul{list-style: none;} li{margin: 10px 0;}</style>
<h1>MHFS Kodi Setup Instructions</h1>
<ol>
<li>Open Kodi</li>
<li>Go to <b>Settings->File manager</b>, <b>Add source</b> (you may have to double-click), and add <b>$baseurl$kodidir</b> (the URL of this page) as a source.</li>
<li>Go to <b>Settings->Add-ons->Install from zip file</b>, open the source you just added, and select <b>$repo_addon_name</b>. The repository add-on should install.</li>
<li>From <b>Settings->Add-ons</b> (you should still be on that page), <b>Install from repository->MHFS Repository->Video add-ons->MHFS Video</b> and click <b>Install</b>. The plugin addon should install.</li>
<li>Click <b>Configure</b> (or open the MHFS Video settings) and fill in <b>$baseurl</b> (the URL of the MHFS server you want to connect to).</li>
<li>MHFS Video should now be installed, you should be able to access it from <b>Add-ons->Video add-ons->MHFS Video</b> on the main menu</li>
</ol>
<ul>
<a href="$repo_addon_name">$repo_addon_name</a>
</ul>
END_HTML
$request->SendHTML($html);
return;
} elsif (substr($request_path, length($kodidir)+1) ne $repo_addon_name ||
substr($request->{'path'}{'unescapepath'}, -1) eq '/') {
$request->Send404;
return;
}
my $xml = <<"END_XML";
<?xml version="1.0" encoding="UTF-8"?>
<addon id="repository.mhfs"
name="MHFS Repository"
version="$repo_addon_name"
provider-name="G4Vi">
<extension point="xbmc.addon.repository" name="MHFS Repository">
<dir>
<info>$baseurl/static/kodi/addons.xml</info>
<checksum>$baseurl/static/kodi/addons.xml.md5</checksum>
<datadir zip="true">$baseurl/static/kodi</datadir>
</dir>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en_GB">MHFS Repository</summary>
<description lang="en_GB">TODO</description>
<disclaimer></disclaimer>
<platform>all</platform>
<language></language>
<license>GPL-2.0-or-later</license>
<forum>https://github.com/G4Vi/MHFS/issues</forum>
<website>computoid.com</website>
<source>https://github.com/G4Vi/MHFS</source>
</extension>
</addon>
END_XML
my $tmpdir = $request->{client}{server}{settings}{GENERIC_TMPDIR};
say "tmpdir $tmpdir";
my $addondir = "$tmpdir/repository.mhfs";
make_path($addondir);
open(my $fh, '>', "$addondir/addon.xml") or do {
warn "failed to open $addondir/addon.xml";
$request->Send404;
return;
};
print $fh $xml;
close($fh) or do {
warn "failed to close";
$request->Send404;
return;
};
_zip_Promise($request->{client}{server}, $tmpdir, ['repository.mhfs'])->then(sub {
$request->SendBytes('application/zip', $_[0]);
}, sub {
warn $_[0];
$request->Send404;
});
}
sub _zip {
my ($server, $start_in, $params, $on_success, $on_failure) = @_;
MHFS::Process->new_output_child($server->{evp}, sub {
# done in child
my ($datachannel) = @_;
chdir($start_in);
open(STDOUT, ">&", $datachannel) or die("Can't dup \$datachannel to STDOUT");
exec('zip', '-r', '-', @$params);
#exec('zip', '-r', 'repository.mhfs.zip', 'repository.mhfs');
die "failed to run zip";
}, sub {
my ($out, $err, $status) = @_;
if ($status != 0) {
$on_failure->('failed to zip');
return;
}
$on_success->($out);
}) // $on_failure->('failed to fork');
}
sub _zip_Promise {
my ($server, $start_in, $params) = @_;
return MHFS::Promise->new($server->{evp}, sub {
my ($resolve, $reject) = @_;
_zip($server, $start_in, $params, sub {
$resolve->($_[0]);
}, sub {
$reject->($_[0]);
});
});
}
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;
lib/MHFS/Plugin/Kodi.pm view on Meta::CPAN
sub route_metadata {
my ($self, $request) = @_;
my $request_path = do {
try { decode_utf_8($request->{path}{unsafepath}) }
catch($e) {
warn "$request->{path}{unsafepath} is not, UTF-8, 400";
$request->Send400;
return;
}
};
my ($mediatype, $metadatatype, $medianame, $season, $episode) = $request_path =~ m!^/kodi/metadata/(movies|tv)/(thumb|fanart|plot)/([^/]+)(?:/0*(\d+)(?:/0*(\d+))?)?$! or do {
say "no match";
$request->Send400;
return;
};
if ($medianame =~ /^.(.)?$/ || ($mediatype eq 'movies' && defined $season)) {
say "no match";
$request->Send400;
return;
}
if ($metadatatype eq 'fanart') {
($season, $episode) = (undef, undef);
}
$medianame = fold_case($medianame);
say "mt $mediatype mmt $metadatatype mn $medianame". (defined $season ? " season $season". (defined $episode ? " episode $episode" : '') : '');
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) : '') : '');
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) {
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) {
$request->SendLocalFile($path);
return;
}
}
}
}
# slow path, download it
$request->{client}{server}{settings}{TMDB} or do {
$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 {
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 {
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?"; }
}
$episode // return $_[0];
MHFS::Kodi::Season::_get_season_episode($_[0], $episode)
})
})->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($metadatatype eq 'plot') {
$request->SendText('text/plain; charset=utf-8', $_[0]->{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;
});
});
})->then(undef, sub {
print $_[0];
$request->Send404;
return;
});
return;
}
sub new {
my ($class, $settings) = @_;
my $self = {};
bless $self, $class;
my @subsystems = ('video');
$self->{moviemeta} = $settings->{'DATADIR'}.'/movies';
$self->{tvmeta} = $settings->{'DATADIR'}.'/tv';
make_path($self->{moviemeta}, $self->{tvmeta});
$self->{'routes'} = [
DirectoryRoute('/kodi/movies', sub {
my ($request) = @_;
route_movies($self, $request, $settings->{'MEDIASOURCES'}{'movies'}, '/kodi/movies');
}),
DirectoryRoute('/kodi/tv', sub {
my ($request) = @_;
route_tv($self, $request, $settings->{'MEDIASOURCES'}{'tv'}, '/kodi/tv');
}),
['/kodi/metadata/*', sub {
my ($request) = @_;
route_metadata($self, $request);
}],
DirectoryRoute('/kodi', sub {
my ($request) = @_;
route_kodi($self, $request, '/kodi');
}),
];
return $self;
}
1;
( run in 1.892 second using v1.01-cache-2.11-cpan-39bf76dae61 )