Comparar commits
43 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| d10903c9fc | |||
| 91f7d9594c | |||
| e041b0dd22 | |||
| d0b99faeeb | |||
| 69df15f4cd | |||
| 14bf3075bd | |||
| fe37299fb3 | |||
| ec341f6a0e | |||
| 0c1d8bf942 | |||
| c3e5e96c2c | |||
| 245ab8728e | |||
| 782b01a022 | |||
| 07c0b0146d | |||
| fb00f6707a | |||
| 2b980603a6 | |||
| 6b9f69d3c4 | |||
| 03b8155000 | |||
| a60ec93bef | |||
| b7b6237142 | |||
| 984b124f9b | |||
| fa2af6de56 | |||
| 9195d7bb4a | |||
| fab941b13b | |||
| 7b901b85a8 | |||
| 69cfe03047 | |||
| 48ae1101e4 | |||
| 243f5fa231 | |||
| ad4e60ae82 | |||
| 64c64c821d | |||
| 565dcfd83c | |||
| 9ac917318c | |||
| f52110ae5f | |||
| 7ae7550410 | |||
| 5360ca0053 | |||
| 3347b0737e | |||
| 80010c63ba | |||
| c4155faa88 | |||
| c1025bb669 | |||
| 71db0b2c14 | |||
| ad3a191c07 | |||
| 6ec35f1dfb | |||
| 95bd129ad4 | |||
| 47f284e38b |
+3
-2
@@ -1,7 +1,7 @@
|
||||
!Build/
|
||||
.last_cover_stats
|
||||
/META.yml
|
||||
/META.json
|
||||
#/META.yml
|
||||
#/META.json
|
||||
/MYMETA.*
|
||||
*.o
|
||||
*.pm.tdy
|
||||
@@ -33,3 +33,4 @@ inc/
|
||||
/MANIFEST.bak
|
||||
/pm_to_blib
|
||||
/*.zip
|
||||
/bin/*.json
|
||||
|
||||
+22
@@ -5,6 +5,28 @@
|
||||
|
||||
[CHANGELOG]
|
||||
|
||||
Version 0.0.4
|
||||
|
||||
- Support for YouTube usernames (-u=username).
|
||||
- No longer ignore live stream videos in search results.
|
||||
- Display the relative age of videos in search results.
|
||||
|
||||
Version 0.0.3
|
||||
|
||||
- Support for multiple search parameters.
|
||||
- Support for searching for videos from a given channel (with --author=channelID).
|
||||
- Support for next pages in more contexts.
|
||||
- Performance improvements and bug-fixes.
|
||||
|
||||
Version 0.0.2
|
||||
|
||||
- Support for next pages.
|
||||
- Support for newest videos from a channel.
|
||||
- Support for popular videos from a channel.
|
||||
- Support for playlists from a channel.
|
||||
- Support for listing a playlist of videos.
|
||||
- Searching support for playlists and channels.
|
||||
|
||||
Version 0.0.1
|
||||
|
||||
- To be released soon.
|
||||
|
||||
@@ -9,6 +9,7 @@ lib/WWW/PipeViewer/Channels.pm
|
||||
lib/WWW/PipeViewer/CommentThreads.pm
|
||||
lib/WWW/PipeViewer/GetCaption.pm
|
||||
lib/WWW/PipeViewer/GuideCategories.pm
|
||||
lib/WWW/PipeViewer/InitialData.pm
|
||||
lib/WWW/PipeViewer/Itags.pm
|
||||
lib/WWW/PipeViewer/ParseJSON.pm
|
||||
lib/WWW/PipeViewer/ParseXML.pm
|
||||
@@ -37,3 +38,5 @@ share/icons/user.png
|
||||
t/00-load.t
|
||||
t/kwalitee.t
|
||||
t/pod.t
|
||||
utils/auto_perltidy.sh
|
||||
utils/bak_cleaner.sh
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"abstract" : "A very easy interface to YouTube, using the API of invidio.us.",
|
||||
"author" : [
|
||||
"Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>"
|
||||
],
|
||||
"dynamic_config" : 1,
|
||||
"generated_by" : "Module::Build version 0.4231",
|
||||
"license" : [
|
||||
"perl_5"
|
||||
],
|
||||
"meta-spec" : {
|
||||
"url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
|
||||
"version" : 2
|
||||
},
|
||||
"name" : "WWW-PipeViewer",
|
||||
"prereqs" : {
|
||||
"build" : {
|
||||
"requires" : {
|
||||
"Test::More" : "0"
|
||||
}
|
||||
},
|
||||
"configure" : {
|
||||
"requires" : {
|
||||
"Module::Build" : "0"
|
||||
}
|
||||
},
|
||||
"runtime" : {
|
||||
"recommends" : {
|
||||
"JSON::XS" : "0",
|
||||
"LWP::UserAgent::Cached" : "0",
|
||||
"Mozilla::CA" : "0",
|
||||
"Term::ReadLine::Gnu" : "0"
|
||||
},
|
||||
"requires" : {
|
||||
"Data::Dump" : "0",
|
||||
"Encode" : "0",
|
||||
"File::Path" : "0",
|
||||
"File::Spec" : "0",
|
||||
"File::Spec::Functions" : "0",
|
||||
"Getopt::Long" : "0",
|
||||
"HTTP::Request" : "0",
|
||||
"JSON" : "0",
|
||||
"LWP::Protocol::https" : "0",
|
||||
"LWP::UserAgent" : "0",
|
||||
"List::Util" : "0",
|
||||
"MIME::Base64" : "0",
|
||||
"Memoize" : "0",
|
||||
"Term::ANSIColor" : "0",
|
||||
"Term::ReadLine" : "0",
|
||||
"Text::ParseWords" : "0",
|
||||
"Text::Wrap" : "0",
|
||||
"URI::Escape" : "0",
|
||||
"perl" : "5.016"
|
||||
}
|
||||
}
|
||||
},
|
||||
"provides" : {
|
||||
"WWW::PipeViewer" : {
|
||||
"file" : "lib/WWW/PipeViewer.pm",
|
||||
"version" : "v0.0.4"
|
||||
},
|
||||
"WWW::PipeViewer::Activities" : {
|
||||
"file" : "lib/WWW/PipeViewer/Activities.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Authentication" : {
|
||||
"file" : "lib/WWW/PipeViewer/Authentication.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Channels" : {
|
||||
"file" : "lib/WWW/PipeViewer/Channels.pm"
|
||||
},
|
||||
"WWW::PipeViewer::CommentThreads" : {
|
||||
"file" : "lib/WWW/PipeViewer/CommentThreads.pm"
|
||||
},
|
||||
"WWW::PipeViewer::GetCaption" : {
|
||||
"file" : "lib/WWW/PipeViewer/GetCaption.pm"
|
||||
},
|
||||
"WWW::PipeViewer::GuideCategories" : {
|
||||
"file" : "lib/WWW/PipeViewer/GuideCategories.pm"
|
||||
},
|
||||
"WWW::PipeViewer::InitialData" : {
|
||||
"file" : "lib/WWW/PipeViewer/InitialData.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Itags" : {
|
||||
"file" : "lib/WWW/PipeViewer/Itags.pm"
|
||||
},
|
||||
"WWW::PipeViewer::ParseJSON" : {
|
||||
"file" : "lib/WWW/PipeViewer/ParseJSON.pm"
|
||||
},
|
||||
"WWW::PipeViewer::ParseXML" : {
|
||||
"file" : "lib/WWW/PipeViewer/ParseXML.pm"
|
||||
},
|
||||
"WWW::PipeViewer::PlaylistItems" : {
|
||||
"file" : "lib/WWW/PipeViewer/PlaylistItems.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Playlists" : {
|
||||
"file" : "lib/WWW/PipeViewer/Playlists.pm"
|
||||
},
|
||||
"WWW::PipeViewer::RegularExpressions" : {
|
||||
"file" : "lib/WWW/PipeViewer/RegularExpressions.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Search" : {
|
||||
"file" : "lib/WWW/PipeViewer/Search.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Subscriptions" : {
|
||||
"file" : "lib/WWW/PipeViewer/Subscriptions.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Utils" : {
|
||||
"file" : "lib/WWW/PipeViewer/Utils.pm"
|
||||
},
|
||||
"WWW::PipeViewer::VideoCategories" : {
|
||||
"file" : "lib/WWW/PipeViewer/VideoCategories.pm"
|
||||
},
|
||||
"WWW::PipeViewer::Videos" : {
|
||||
"file" : "lib/WWW/PipeViewer/Videos.pm"
|
||||
}
|
||||
},
|
||||
"release_status" : "stable",
|
||||
"resources" : {
|
||||
"license" : [
|
||||
"http://dev.perl.org/licenses/"
|
||||
]
|
||||
},
|
||||
"version" : "v0.0.4",
|
||||
"x_serialization_backend" : "JSON::PP version 4.05"
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
abstract: 'A very easy interface to YouTube, using the API of invidio.us.'
|
||||
author:
|
||||
- 'Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>'
|
||||
build_requires:
|
||||
Test::More: '0'
|
||||
configure_requires:
|
||||
Module::Build: '0'
|
||||
dynamic_config: 1
|
||||
generated_by: 'Module::Build version 0.4231, CPAN::Meta::Converter version 2.150010'
|
||||
license: perl
|
||||
meta-spec:
|
||||
url: http://module-build.sourceforge.net/META-spec-v1.4.html
|
||||
version: '1.4'
|
||||
name: WWW-PipeViewer
|
||||
provides:
|
||||
WWW::PipeViewer:
|
||||
file: lib/WWW/PipeViewer.pm
|
||||
version: v0.0.4
|
||||
WWW::PipeViewer::Activities:
|
||||
file: lib/WWW/PipeViewer/Activities.pm
|
||||
WWW::PipeViewer::Authentication:
|
||||
file: lib/WWW/PipeViewer/Authentication.pm
|
||||
WWW::PipeViewer::Channels:
|
||||
file: lib/WWW/PipeViewer/Channels.pm
|
||||
WWW::PipeViewer::CommentThreads:
|
||||
file: lib/WWW/PipeViewer/CommentThreads.pm
|
||||
WWW::PipeViewer::GetCaption:
|
||||
file: lib/WWW/PipeViewer/GetCaption.pm
|
||||
WWW::PipeViewer::GuideCategories:
|
||||
file: lib/WWW/PipeViewer/GuideCategories.pm
|
||||
WWW::PipeViewer::InitialData:
|
||||
file: lib/WWW/PipeViewer/InitialData.pm
|
||||
WWW::PipeViewer::Itags:
|
||||
file: lib/WWW/PipeViewer/Itags.pm
|
||||
WWW::PipeViewer::ParseJSON:
|
||||
file: lib/WWW/PipeViewer/ParseJSON.pm
|
||||
WWW::PipeViewer::ParseXML:
|
||||
file: lib/WWW/PipeViewer/ParseXML.pm
|
||||
WWW::PipeViewer::PlaylistItems:
|
||||
file: lib/WWW/PipeViewer/PlaylistItems.pm
|
||||
WWW::PipeViewer::Playlists:
|
||||
file: lib/WWW/PipeViewer/Playlists.pm
|
||||
WWW::PipeViewer::RegularExpressions:
|
||||
file: lib/WWW/PipeViewer/RegularExpressions.pm
|
||||
WWW::PipeViewer::Search:
|
||||
file: lib/WWW/PipeViewer/Search.pm
|
||||
WWW::PipeViewer::Subscriptions:
|
||||
file: lib/WWW/PipeViewer/Subscriptions.pm
|
||||
WWW::PipeViewer::Utils:
|
||||
file: lib/WWW/PipeViewer/Utils.pm
|
||||
WWW::PipeViewer::VideoCategories:
|
||||
file: lib/WWW/PipeViewer/VideoCategories.pm
|
||||
WWW::PipeViewer::Videos:
|
||||
file: lib/WWW/PipeViewer/Videos.pm
|
||||
recommends:
|
||||
JSON::XS: '0'
|
||||
LWP::UserAgent::Cached: '0'
|
||||
Mozilla::CA: '0'
|
||||
Term::ReadLine::Gnu: '0'
|
||||
requires:
|
||||
Data::Dump: '0'
|
||||
Encode: '0'
|
||||
File::Path: '0'
|
||||
File::Spec: '0'
|
||||
File::Spec::Functions: '0'
|
||||
Getopt::Long: '0'
|
||||
HTTP::Request: '0'
|
||||
JSON: '0'
|
||||
LWP::Protocol::https: '0'
|
||||
LWP::UserAgent: '0'
|
||||
List::Util: '0'
|
||||
MIME::Base64: '0'
|
||||
Memoize: '0'
|
||||
Term::ANSIColor: '0'
|
||||
Term::ReadLine: '0'
|
||||
Text::ParseWords: '0'
|
||||
Text::Wrap: '0'
|
||||
URI::Escape: '0'
|
||||
perl: '5.016'
|
||||
resources:
|
||||
license: http://dev.perl.org/licenses/
|
||||
version: v0.0.4
|
||||
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
|
||||
+6
-8
@@ -2,27 +2,25 @@
|
||||
|
||||
A lightweight application (fork of [straw-viewer](https://github.com/trizen/straw-viewer)) for searching and playing videos from YouTube, using the [API](https://github.com/iv-org/invidious/wiki/API) of [invidio.us](https://invidio.us/).
|
||||
|
||||
The goal of this fork is to parse the YouTube website directly, removing the dependency on invidious instances.
|
||||
The goal of this fork is to parse the YouTube website directly and rely on the invidious instances only as a fallback method.
|
||||
|
||||
### pipe-viewer
|
||||
|
||||
* command-line interface to YouTube.
|
||||
|
||||

|
||||

|
||||
|
||||
### gtk-pipe-viewer
|
||||
|
||||
* GTK+ interface to YouTube.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
### STATUS
|
||||
|
||||
The project is in its early stages of development and some features are not implemented yet.
|
||||
|
||||
Currently, only the searching for videos uses the YouTube website directly.
|
||||
|
||||
|
||||
### AVAILABILITY
|
||||
|
||||
@@ -54,9 +52,9 @@ For trying the latest commit of `pipe-viewer`, without installing it, execute th
|
||||
|
||||
```console
|
||||
cd /tmp
|
||||
wget https://github.com/trizen/pipe-viewer/archive/master.zip -O pipe-viewer-master.zip
|
||||
unzip -n pipe-viewer-master.zip
|
||||
cd pipe-viewer-master/bin
|
||||
wget https://github.com/trizen/pipe-viewer/archive/main.zip -O pipe-viewer-main.zip
|
||||
unzip -n pipe-viewer-main.zip
|
||||
cd pipe-viewer-main/bin
|
||||
perl -pi -ne 's{DEVEL = 0}{DEVEL = 1}' {gtk-,}pipe-viewer
|
||||
./pipe-viewer
|
||||
```
|
||||
|
||||
+87
-73
@@ -15,12 +15,12 @@
|
||||
#-------------------------------------------------------
|
||||
# GTK Pipe Viewer
|
||||
# Fork: 30 October 2020
|
||||
# Edit: 30 October 2020
|
||||
# Edit: 27 November 2020
|
||||
# https://github.com/trizen/pipe-viewer
|
||||
#-------------------------------------------------------
|
||||
|
||||
# This is a fork of youtube-viewer:
|
||||
# https://github.com/trizen/youtube-viewer
|
||||
# This is a fork of straw-viewer:
|
||||
# https://github.com/trizen/straw-viewer
|
||||
|
||||
use utf8;
|
||||
use 5.016;
|
||||
@@ -31,7 +31,7 @@ no warnings 'once';
|
||||
my $DEVEL; # true in devel mode
|
||||
use if ($DEVEL = 1), lib => qw(../lib); # devel only
|
||||
|
||||
use WWW::PipeViewer v0.0.1;
|
||||
use WWW::PipeViewer v0.0.4;
|
||||
use WWW::PipeViewer::RegularExpressions;
|
||||
|
||||
use Gtk3 qw(-init);
|
||||
@@ -151,21 +151,15 @@ my %CONFIG = (
|
||||
srt => q{--sub-file=*SUB*},
|
||||
audio => q{--input-slave=*AUDIO*},
|
||||
fs => q{--fullscreen},
|
||||
arg => q{--quiet --play-and-exit --no-video-title-show --input-title-format=*TITLE*},
|
||||
arg => q{--quiet --play-and-exit --no-video-title-show --input-title-format=*TITLE* *VIDEO*},
|
||||
},
|
||||
mpv => {
|
||||
cmd => q{mpv},
|
||||
srt => q{--sub-file=*SUB*},
|
||||
audio => q{--audio-file=*AUDIO*},
|
||||
fs => q{--fullscreen},
|
||||
arg => q{--really-quiet --force-media-title=*TITLE* --no-ytdl},
|
||||
arg => q{--really-quiet --force-media-title=*TITLE* --no-ytdl *VIDEO*},
|
||||
},
|
||||
smplayer => {
|
||||
cmd => q{smplayer},
|
||||
srt => q{-sub *SUB*},
|
||||
fs => q{-fullscreen},
|
||||
arg => q{-close-at-end -media-title *TITLE* *URL*},
|
||||
},
|
||||
},
|
||||
video_player_selected => undef, # autodetect it later
|
||||
|
||||
@@ -182,9 +176,9 @@ my %CONFIG = (
|
||||
hpaned_position => 420,
|
||||
|
||||
# Pipe options
|
||||
dash_support => 1,
|
||||
dash_mp4_audio => 1,
|
||||
dash_segmented => 1, # may load slow
|
||||
split_videos => 1,
|
||||
m4a_audio => 1,
|
||||
dash => 1, # may load slow
|
||||
prefer_mp4 => 0,
|
||||
prefer_av1 => 0,
|
||||
ignore_av1 => 0,
|
||||
@@ -236,8 +230,8 @@ my %CONFIG = (
|
||||
web_browser => undef, # defaults to $ENV{WEBBROWSER} or xdg-open
|
||||
terminal => undef, # autodetect it later
|
||||
terminal_exec => q{-e '%s'},
|
||||
pipe_viewer => undef,
|
||||
pipe_viewer_args => [],
|
||||
pipe_viewer => undef,
|
||||
pipe_viewer_args => [],
|
||||
youtube_users_file => $youtube_users_file,
|
||||
history => 1,
|
||||
history_limit => 100_000,
|
||||
@@ -341,6 +335,7 @@ my %objects = (
|
||||
'comboboxtext8' => \my $duration_combobox,
|
||||
'comboboxtext3' => \my $caption_combobox,
|
||||
'comboboxtext4' => \my $definition_combobox,
|
||||
'comboboxtext5' => \my $license_combobox,
|
||||
'comboboxtext1' => \my $published_within_combobox,
|
||||
'comboboxtext13' => \my $subscriptions_order_combobox,
|
||||
'panel_user_entry' => \my $panel_user_entry,
|
||||
@@ -811,16 +806,16 @@ my %ResultsHistory = (
|
||||
$CONFIG{pipe_viewer} //= which_command('pipe-viewer') // 'pipe-viewer';
|
||||
|
||||
my $yv_obj = WWW::PipeViewer->new(
|
||||
escape_utf8 => 1,
|
||||
config_dir => $config_dir,
|
||||
ytdl => $CONFIG{ytdl},
|
||||
ytdl_cmd => $CONFIG{ytdl_cmd},
|
||||
env_proxy => $CONFIG{env_proxy},
|
||||
cache_dir => $CONFIG{cache_dir},
|
||||
cookie_file => $CONFIG{cookie_file},
|
||||
user_agent => $CONFIG{user_agent},
|
||||
timeout => $CONFIG{timeout},
|
||||
);
|
||||
escape_utf8 => 1,
|
||||
config_dir => $config_dir,
|
||||
ytdl => $CONFIG{ytdl},
|
||||
ytdl_cmd => $CONFIG{ytdl_cmd},
|
||||
env_proxy => $CONFIG{env_proxy},
|
||||
cache_dir => $CONFIG{cache_dir},
|
||||
cookie_file => $CONFIG{cookie_file},
|
||||
user_agent => $CONFIG{user_agent},
|
||||
timeout => $CONFIG{timeout},
|
||||
);
|
||||
|
||||
#$yv_obj->load_authentication_tokens();
|
||||
|
||||
@@ -833,7 +828,7 @@ else {
|
||||
|
||||
require WWW::PipeViewer::Utils;
|
||||
my $yv_utils = WWW::PipeViewer::Utils->new(thousand_separator => $CONFIG{thousand_separator},
|
||||
youtube_url_format => $CONFIG{youtube_video_url},);
|
||||
youtube_url_format => $CONFIG{youtube_video_url},);
|
||||
|
||||
# Set default combobox values
|
||||
$definition_combobox->set_active(0);
|
||||
@@ -853,7 +848,7 @@ sub apply_configuration {
|
||||
$audio_only_checkbutton->set_active($CONFIG{audio_only});
|
||||
|
||||
# DASH mode
|
||||
$dash_checkbutton->set_active($CONFIG{dash_support});
|
||||
$dash_checkbutton->set_active($CONFIG{dash});
|
||||
|
||||
$clear_list_checkbutton->set_active($CONFIG{clear_search_list});
|
||||
$panel_account_type_combobox->set_active($CONFIG{active_panel_account_combobox});
|
||||
@@ -1226,6 +1221,16 @@ sub menu_popup {
|
||||
$author->append($item);
|
||||
}
|
||||
|
||||
# Playlists created by this author
|
||||
{
|
||||
my $item = 'Gtk3::ImageMenuItem'->new("Playlists");
|
||||
$item->signal_connect(activate => \&show_playlists_from_selected_author);
|
||||
$item->set_property(tooltip_text => "Show playlists created by this author");
|
||||
$item->set_image('Gtk3::Image'->new_from_icon_name("emblem-documents-symbolic", q{menu}));
|
||||
$item->show;
|
||||
$author->append($item);
|
||||
}
|
||||
|
||||
# Favorites of this author
|
||||
{
|
||||
my $item = 'Gtk3::ImageMenuItem'->new("Favorites");
|
||||
@@ -1237,34 +1242,28 @@ sub menu_popup {
|
||||
}
|
||||
|
||||
# Recent channel activity events
|
||||
{
|
||||
my $item = 'Gtk3::ImageMenuItem'->new("Activities");
|
||||
$item->signal_connect(activate => sub { activities('channel', $channel_id) });
|
||||
$item->set_property(tooltip_text => "Show recent channel activity events");
|
||||
$item->set_image('Gtk3::Image'->new_from_icon_name("view-refresh-symbolic", q{menu}));
|
||||
$item->show;
|
||||
$author->append($item);
|
||||
}
|
||||
|
||||
# Playlists created by this author
|
||||
{
|
||||
my $item = 'Gtk3::ImageMenuItem'->new("Playlists");
|
||||
$item->signal_connect(activate => \&show_playlists_from_selected_author);
|
||||
$item->set_property(tooltip_text => "Show playlists created by this author");
|
||||
$item->set_image('Gtk3::Image'->new_from_icon_name("emblem-documents-symbolic", q{menu}));
|
||||
$item->show;
|
||||
$author->append($item);
|
||||
}
|
||||
#<<<
|
||||
#~ {
|
||||
#~ my $item = 'Gtk3::ImageMenuItem'->new("Activities");
|
||||
#~ $item->signal_connect(activate => sub { activities('channel', $channel_id) });
|
||||
#~ $item->set_property(tooltip_text => "Show recent channel activity events");
|
||||
#~ $item->set_image('Gtk3::Image'->new_from_icon_name("view-refresh-symbolic", q{menu}));
|
||||
#~ $item->show;
|
||||
#~ $author->append($item);
|
||||
#~ }
|
||||
#>>>
|
||||
|
||||
# Liked videos by this author
|
||||
{
|
||||
my $item = 'Gtk3::ImageMenuItem'->new("Likes");
|
||||
$item->signal_connect(activate => sub { likes('channel', $channel_id) });
|
||||
$item->set_property(tooltip_text => "Show liked videos by this author");
|
||||
$item->set_image('Gtk3::Image'->new_from_icon_name("emblem-default-symbolic", q{menu}));
|
||||
$item->show;
|
||||
$author->append($item);
|
||||
}
|
||||
#<<<
|
||||
#~ {
|
||||
#~ my $item = 'Gtk3::ImageMenuItem'->new("Likes");
|
||||
#~ $item->signal_connect(activate => sub { likes('channel', $channel_id) });
|
||||
#~ $item->set_property(tooltip_text => "Show liked videos by this author");
|
||||
#~ $item->set_image('Gtk3::Image'->new_from_icon_name("emblem-default-symbolic", q{menu}));
|
||||
#~ $item->show;
|
||||
#~ $author->append($item);
|
||||
#~ }
|
||||
#>>>
|
||||
|
||||
# Separator
|
||||
{
|
||||
@@ -1341,9 +1340,13 @@ sub menu_popup {
|
||||
my $item = 'Gtk3::ImageMenuItem'->new("Play as audio");
|
||||
$item->signal_connect(
|
||||
activate => sub {
|
||||
my ($video_id, $iter) = get_selected_entry_code();
|
||||
if (defined($video_id) and $liststore->get($iter, 7) eq 'video') {
|
||||
execute_cli_pipe_viewer("--id=$video_id --no-video");
|
||||
my ($id, $iter) = get_selected_entry_code();
|
||||
my $type = $liststore->get($iter, 7);
|
||||
if (defined($id) and $type eq 'video') {
|
||||
execute_cli_pipe_viewer("--id=$id --no-video");
|
||||
}
|
||||
elsif (defined($id) and $type eq 'playlist') {
|
||||
execute_cli_pipe_viewer("--pp=$id --no-video");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1697,6 +1700,11 @@ sub combobox_definition_changed {
|
||||
$yv_obj->set_videoDefinition($text);
|
||||
}
|
||||
|
||||
sub combobox_license_changed {
|
||||
my $text = $license_combobox->get_active_text;
|
||||
$yv_obj->set_videoLicense($text);
|
||||
}
|
||||
|
||||
sub combobox_published_within_changed {
|
||||
|
||||
my $period = $published_within_combobox->get_active_text;
|
||||
@@ -1736,7 +1744,7 @@ sub toggled_audio_only {
|
||||
|
||||
# DASH mode
|
||||
sub toggled_dash_support {
|
||||
$CONFIG{dash_support} = $dash_checkbutton->get_active() || 0;
|
||||
$CONFIG{dash} = $dash_checkbutton->get_active() || 0;
|
||||
}
|
||||
|
||||
# Check buttons toggles
|
||||
@@ -2465,6 +2473,12 @@ sub display_results {
|
||||
elsif (exists $items->{playlists}) {
|
||||
$items = $items->{playlists};
|
||||
}
|
||||
elsif (exists $items->{channels}) {
|
||||
$items = $items->{channels};
|
||||
}
|
||||
elsif (exists $items->{entries}) {
|
||||
$items = $items->{entries};
|
||||
}
|
||||
else {
|
||||
warn "No results...\n";
|
||||
}
|
||||
@@ -2953,12 +2967,12 @@ sub get_streaming_url {
|
||||
if (ref($captions) eq 'ARRAY' and @$captions and $CONFIG{get_captions}) {
|
||||
require WWW::PipeViewer::GetCaption;
|
||||
my $yv_cap = WWW::PipeViewer::GetCaption->new(
|
||||
auto_captions => $CONFIG{auto_captions},
|
||||
captions_dir => $CONFIG{cache_dir},
|
||||
captions => $captions,
|
||||
languages => $CONFIG{srt_languages},
|
||||
yv_obj => $yv_obj,
|
||||
);
|
||||
auto_captions => $CONFIG{auto_captions},
|
||||
captions_dir => $CONFIG{cache_dir},
|
||||
captions => $captions,
|
||||
languages => $CONFIG{srt_languages},
|
||||
yv_obj => $yv_obj,
|
||||
);
|
||||
$srt_file = $yv_cap->save_caption($video_id);
|
||||
}
|
||||
|
||||
@@ -2972,9 +2986,9 @@ sub get_streaming_url {
|
||||
hfr => $CONFIG{hfr},
|
||||
ignore_av1 => $CONFIG{ignore_av1},
|
||||
|
||||
dash => $CONFIG{dash_support},
|
||||
dash_mp4_audio => $CONFIG{dash_mp4_audio},
|
||||
dash_segmented => $CONFIG{dash_segmented},
|
||||
split => $CONFIG{split_videos},
|
||||
m4a_audio => $CONFIG{m4a_audio},
|
||||
dash => $CONFIG{dash},
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -3024,7 +3038,7 @@ sub get_player_command {
|
||||
)
|
||||
);
|
||||
|
||||
my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;
|
||||
my $has_video = $cmd =~ /(?:^|\s)\*(?:VIDEO|URL)\*(?:\s|\z)/;
|
||||
|
||||
$cmd = $yv_utils->format_text(
|
||||
streaming => $streaming,
|
||||
@@ -3138,9 +3152,9 @@ sub get_options_as_arguments {
|
||||
'no-interactive' => q{},
|
||||
'resolution' => $CONFIG{resolution},
|
||||
'download-dir' => quotemeta(rel2abs($CONFIG{downloads_dir})),
|
||||
'fullscreen' => $CONFIG{fullscreen} ? q{} : undef,
|
||||
'no-dash' => $CONFIG{dash_support} ? undef : q{},
|
||||
'no-video' => $CONFIG{audio_only} ? q{} : undef,
|
||||
'fullscreen' => $CONFIG{fullscreen} ? q{} : undef,
|
||||
'no-dash' => $CONFIG{dash} ? undef : q{},
|
||||
'no-video' => $CONFIG{audio_only} ? q{} : undef,
|
||||
);
|
||||
|
||||
while (my ($argv, $value) = each %options) {
|
||||
@@ -3244,7 +3258,7 @@ sub execute_cli_pipe_viewer {
|
||||
$CONFIG{terminal_exec},
|
||||
join(q{ },
|
||||
$CONFIG{pipe_viewer}, get_options_as_arguments(),
|
||||
@arguments, @{$CONFIG{pipe_viewer_args}}),
|
||||
@arguments, @{$CONFIG{pipe_viewer_args}}),
|
||||
)
|
||||
);
|
||||
my $code = execute_external_program($command);
|
||||
|
||||
+170
-98
@@ -14,15 +14,15 @@
|
||||
#
|
||||
#-------------------------------------------------------
|
||||
# pipe-viewer
|
||||
# Fork: 14 February 2020
|
||||
# Edit: 06 October 2020
|
||||
# Fork: 30 October 2020
|
||||
# Edit: 27 November 2020
|
||||
# https://github.com/trizen/pipe-viewer
|
||||
#-------------------------------------------------------
|
||||
|
||||
# pipe-viewer is a command line utility for streaming YouTube videos in mpv/vlc.
|
||||
|
||||
# This is a fork of youtube-viewer:
|
||||
# https://github.com/trizen/youtube-viewer
|
||||
# This is a fork of straw-viewer:
|
||||
# https://github.com/trizen/straw-viewer
|
||||
|
||||
=encoding utf8
|
||||
|
||||
@@ -46,7 +46,7 @@ no warnings 'once';
|
||||
my $DEVEL; # true in devel mode
|
||||
use if ($DEVEL = 1), lib => qw(../lib); # devel mode
|
||||
|
||||
use WWW::PipeViewer v0.0.1;
|
||||
use WWW::PipeViewer v0.0.4;
|
||||
use WWW::PipeViewer::RegularExpressions;
|
||||
|
||||
use File::Spec::Functions qw(
|
||||
@@ -144,7 +144,7 @@ my %CONFIG = (
|
||||
srt => q{--sub-file=*SUB*},
|
||||
audio => q{--input-slave=*AUDIO*},
|
||||
fs => q{--fullscreen},
|
||||
arg => q{--quiet --play-and-exit --no-video-title-show --input-title-format=*TITLE*},
|
||||
arg => q{--quiet --play-and-exit --no-video-title-show --input-title-format=*TITLE* *VIDEO*},
|
||||
novideo => q{--intf=dummy --novideo},
|
||||
},
|
||||
mpv => {
|
||||
@@ -152,7 +152,7 @@ my %CONFIG = (
|
||||
srt => q{--sub-file=*SUB*},
|
||||
audio => q{--audio-file=*AUDIO*},
|
||||
fs => q{--fullscreen},
|
||||
arg => q{--really-quiet --force-media-title=*TITLE* --no-ytdl},
|
||||
arg => q{--really-quiet --force-media-title=*TITLE* --no-ytdl *VIDEO*},
|
||||
novideo => q{--no-video},
|
||||
},
|
||||
},
|
||||
@@ -163,10 +163,11 @@ my %CONFIG = (
|
||||
: undef # auto-defined
|
||||
),
|
||||
|
||||
split_videos => 1,
|
||||
m4a_audio => 1,
|
||||
|
||||
# YouTube options
|
||||
dash_support => 1,
|
||||
dash_mp4_audio => 1,
|
||||
dash_segmented => 1, # may load slow
|
||||
dash => 1, # may load slow
|
||||
maxResults => 20,
|
||||
hfr => 1, # true to prefer high frame rate (HFR) videos
|
||||
resolution => 'best',
|
||||
@@ -604,21 +605,21 @@ if ($opt{history}) {
|
||||
}
|
||||
|
||||
my $yv_obj = WWW::PipeViewer->new(
|
||||
escape_utf8 => 1,
|
||||
config_dir => $config_dir,
|
||||
ytdl => $opt{ytdl},
|
||||
ytdl_cmd => $opt{ytdl_cmd},
|
||||
cache_dir => $opt{cache_dir},
|
||||
env_proxy => $opt{env_proxy},
|
||||
cookie_file => $opt{cookie_file},
|
||||
http_proxy => $opt{http_proxy},
|
||||
user_agent => $opt{user_agent},
|
||||
timeout => $opt{timeout},
|
||||
);
|
||||
escape_utf8 => 1,
|
||||
config_dir => $config_dir,
|
||||
ytdl => $opt{ytdl},
|
||||
ytdl_cmd => $opt{ytdl_cmd},
|
||||
cache_dir => $opt{cache_dir},
|
||||
env_proxy => $opt{env_proxy},
|
||||
cookie_file => $opt{cookie_file},
|
||||
http_proxy => $opt{http_proxy},
|
||||
user_agent => $opt{user_agent},
|
||||
timeout => $opt{timeout},
|
||||
);
|
||||
|
||||
require WWW::PipeViewer::Utils;
|
||||
my $yv_utils = WWW::PipeViewer::Utils->new(youtube_url_format => $opt{youtube_video_url},
|
||||
thousand_separator => $opt{thousand_separator},);
|
||||
thousand_separator => $opt{thousand_separator},);
|
||||
|
||||
{ # Apply the configuration file
|
||||
my %temp = %CONFIG;
|
||||
@@ -806,9 +807,9 @@ usage: $execname [options] ([url] | [keywords])
|
||||
--proxy=s : set HTTP(S)/SOCKS proxy: 'proto://domain.tld:port/'
|
||||
If authentication is required,
|
||||
use 'proto://user:pass\@domain.tld:port/'
|
||||
--dash! : include or exclude the DASH itags
|
||||
--dash-mp4a! : include or exclude the itags for MP4 audio streams
|
||||
--dash-segmented! : include or exclude segmented DASH streams
|
||||
--split-videos! : include or exclude the itags for split videos
|
||||
--m4a-audio! : include or exclude the itags for M4A audio streams
|
||||
--dash! : include or exclude segmented DASH streams
|
||||
--ytdl! : use youtube-dl for videos with encrypted signatures
|
||||
`--no-ytdl` will use invidious instances
|
||||
--ytdl-cmd=s : youtube-dl command (default: youtube-dl)
|
||||
@@ -1537,7 +1538,7 @@ sub parse_arguments {
|
||||
'fixed-width|W|fw!' => \$opt{results_fixed_width},
|
||||
'captions!' => \$opt{videoCaption},
|
||||
'fullscreen|fs|f!' => \$opt{fullscreen},
|
||||
'dash!' => \$opt{dash_support},
|
||||
'split-videos!' => \$opt{split_videos},
|
||||
'confirm!' => \$opt{confirm},
|
||||
|
||||
'prefer-mp4!' => \$opt{prefer_mp4},
|
||||
@@ -1552,23 +1553,23 @@ sub parse_arguments {
|
||||
|
||||
'api-host|instance=s' => \$opt{api_host},
|
||||
|
||||
'convert-command|convert-cmd=s' => \$opt{convert_cmd},
|
||||
'dash-m4a|dash-mp4-audio|dash-mp4a!' => \$opt{dash_mp4_audio},
|
||||
'dash-segmented!' => \$opt{dash_segmented},
|
||||
'wget-dl|wget-download!' => \$opt{download_with_wget},
|
||||
'filename|filename-format=s' => \$opt{video_filename_format},
|
||||
'rp|rem-played|remove-played-file!' => \$opt{remove_played_file},
|
||||
'info|i|video-info=s' => \$opt{print_video_info},
|
||||
'get-term-width!' => \$opt{get_term_width},
|
||||
'page=i' => \$opt{page},
|
||||
'novideo|no-video|n|audio!' => \$opt{novideo},
|
||||
'highlight!' => \$opt{highlight_watched},
|
||||
'skip-watched!' => \$opt{skip_watched},
|
||||
'results=i' => \$opt{maxResults},
|
||||
'shuffle|s!' => \$opt{shuffle},
|
||||
'more|m!' => \$opt{more_results},
|
||||
'pos|position=i' => \$opt{position},
|
||||
'ps|playlist-save=s' => \$opt{playlist_save},
|
||||
'convert-command|convert-cmd=s' => \$opt{convert_cmd},
|
||||
'm4a-audio|mp4-audio!' => \$opt{m4a_audio},
|
||||
'dash|dash-segmented!' => \$opt{dash},
|
||||
'wget-dl|wget-download!' => \$opt{download_with_wget},
|
||||
'filename|filename-format=s' => \$opt{video_filename_format},
|
||||
'rp|rem-played|remove-played-file!' => \$opt{remove_played_file},
|
||||
'info|i|video-info=s' => \$opt{print_video_info},
|
||||
'get-term-width!' => \$opt{get_term_width},
|
||||
'page=i' => \$opt{page},
|
||||
'novideo|no-video|n|audio!' => \$opt{novideo},
|
||||
'highlight!' => \$opt{highlight_watched},
|
||||
'skip-watched!' => \$opt{skip_watched},
|
||||
'results=i' => \$opt{maxResults},
|
||||
'shuffle|s!' => \$opt{shuffle},
|
||||
'more|m!' => \$opt{more_results},
|
||||
'pos|position=i' => \$opt{position},
|
||||
'ps|playlist-save=s' => \$opt{playlist_save},
|
||||
|
||||
'ytdl!' => \$opt{ytdl},
|
||||
'ytdl-cmd=s' => \$opt{ytdl_cmd},
|
||||
@@ -2452,6 +2453,29 @@ sub print_channels {
|
||||
my $url = $results->{url};
|
||||
my $channels = $results->{results} // [];
|
||||
|
||||
if (ref($channels) eq 'HASH') {
|
||||
if (exists $channels->{channels}) {
|
||||
$channels = $channels->{channels};
|
||||
}
|
||||
elsif (exists $channels->{entries}) {
|
||||
$channels = $channels->{entries};
|
||||
}
|
||||
else {
|
||||
warn "\n[!] No channels...\n";
|
||||
$channels = [];
|
||||
}
|
||||
}
|
||||
|
||||
state $info_format = <<"FORMAT";
|
||||
|
||||
TITLE: %s
|
||||
SUBS: %s
|
||||
VIDS: %s
|
||||
ID: %s
|
||||
URL: https://www.youtube.com/channel/%s
|
||||
DESCR: %s
|
||||
FORMAT
|
||||
|
||||
foreach my $i (0 .. $#{$channels}) {
|
||||
my $channel = $channels->[$i];
|
||||
|
||||
@@ -2470,22 +2494,26 @@ sub print_channels {
|
||||
}
|
||||
elsif ($opt{results_with_colors}) {
|
||||
print "\n" if $i == 0;
|
||||
printf("%s. %s (%s)\n",
|
||||
colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
colored($yv_utils->get_channel_title($channel), 'blue'),
|
||||
colored($yv_utils->get_publication_date($channel), 'magenta'),
|
||||
printf(
|
||||
"%s. %s [%s] [%s]\n",
|
||||
colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
colored($yv_utils->get_channel_title($channel), 'blue'),
|
||||
colored($yv_utils->get_publication_date($channel), 'magenta'),
|
||||
colored($yv_utils->short_human_number($yv_utils->get_subscriber_count($channel)), 'green'),
|
||||
);
|
||||
}
|
||||
elsif ($opt{results_fixed_width}) {
|
||||
|
||||
require List::Util;
|
||||
|
||||
my @authors = map { $yv_utils->get_channel_id($_) } @{$channels};
|
||||
my @dates = map { $yv_utils->get_publication_date($_) } @{$channels};
|
||||
my @authors = map { $yv_utils->get_channel_id($_) } @{$channels};
|
||||
my @dates = map { $yv_utils->get_publication_date($_) } @{$channels};
|
||||
my @get_subscriber_count = map { $yv_utils->short_human_number($yv_utils->get_subscriber_count($_)) } @{$channels};
|
||||
|
||||
my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
|
||||
my $dates_width = List::Util::max(map { length($_) } @dates);
|
||||
my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);
|
||||
my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
|
||||
my $dates_width = List::Util::max(map { length($_) } @dates);
|
||||
my $subscriber_count_width = List::Util::max(map { length($_) } @get_subscriber_count);
|
||||
my $title_length = $term_width - ($author_width + $dates_width + $subscriber_count_width + 2 + 3 + 1 + 2 + 2 + 1);
|
||||
|
||||
print "\n";
|
||||
foreach my $i (0 .. $#{$channels}) {
|
||||
@@ -2493,17 +2521,19 @@ sub print_channels {
|
||||
my $channel = $channels->[$i];
|
||||
my $title = clear_title($yv_utils->get_channel_title($channel));
|
||||
|
||||
printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
printf "%s. %s %s [%*s] [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
adjust_width($title, $title_length),
|
||||
adjust_width($authors[$i], $author_width, 1),
|
||||
$dates_width, $dates[$i];
|
||||
$dates_width, $dates[$i],
|
||||
$subscriber_count_width, $get_subscriber_count[$i];
|
||||
}
|
||||
last;
|
||||
}
|
||||
else {
|
||||
print "\n" if $i == 0;
|
||||
printf "%s. %s [%s]\n", colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_channel_title($channel),
|
||||
$yv_utils->get_publication_date($channel);
|
||||
printf "%s. %s [%s] [%s]\n", colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_channel_title($channel),
|
||||
$yv_utils->get_publication_date($channel),
|
||||
$yv_utils->short_human_number($yv_utils->get_subscriber_count($channel));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2530,6 +2560,26 @@ sub print_channels {
|
||||
print $general_help;
|
||||
press_enter_to_continue();
|
||||
}
|
||||
elsif ($opt =~ /^i(?:nfo)?${digit_or_equal_re}(.*)/) {
|
||||
if (my @ids = get_valid_numbers($#{$channels}, $1)) {
|
||||
foreach my $id (@ids) {
|
||||
my $desc = wrap_text(
|
||||
i_tab => q{ } x 7,
|
||||
s_tab => q{ } x 7,
|
||||
text => [$yv_utils->get_description($channels->[$id]) || 'No description available...']
|
||||
);
|
||||
$desc =~ s/^\s+//;
|
||||
printf $info_format, $yv_utils->get_author($channels->[$id]),
|
||||
$yv_utils->short_human_number($yv_utils->get_subscriber_count($channels->[$id])),
|
||||
$yv_utils->short_human_number($yv_utils->get_video_count($channels->[$id])),
|
||||
($yv_utils->get_channel_id($channels->[$id])) x 2, $desc;
|
||||
}
|
||||
press_enter_to_continue();
|
||||
}
|
||||
else {
|
||||
warn_no_thing_selected('channel');
|
||||
}
|
||||
}
|
||||
elsif ($opt =~ /^(?:r|return)\z/) {
|
||||
return;
|
||||
}
|
||||
@@ -2748,6 +2798,9 @@ sub print_playlists {
|
||||
if (exists $playlists->{playlists}) {
|
||||
$playlists = $playlists->{playlists};
|
||||
}
|
||||
elsif (exists $playlists->{entries}) {
|
||||
$playlists = $playlists->{entries};
|
||||
}
|
||||
else {
|
||||
warn "\n[!] No playlists...\n";
|
||||
$playlists = [];
|
||||
@@ -2757,6 +2810,7 @@ sub print_playlists {
|
||||
state $info_format = <<"FORMAT";
|
||||
|
||||
TITLE: %s
|
||||
VIDS: %s
|
||||
ID: %s
|
||||
URL: https://www.youtube.com/playlist?list=%s
|
||||
DESCR: %s
|
||||
@@ -2780,23 +2834,26 @@ FORMAT
|
||||
elsif ($opt{results_with_colors}) {
|
||||
print "\n" if $i == 0;
|
||||
printf(
|
||||
"%s. %s (%s) %s\n",
|
||||
colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
colored($yv_utils->get_title($playlist), 'blue'),
|
||||
colored($yv_utils->get_publication_date($playlist), 'magenta'),
|
||||
colored($yv_utils->get_channel_title($playlist), 'green'),
|
||||
"%s. %s [%s] %s [%s]\n",
|
||||
colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
colored($yv_utils->get_title($playlist), 'blue'),
|
||||
colored($yv_utils->get_publication_date($playlist), 'magenta'),
|
||||
colored($yv_utils->get_channel_title($playlist), 'green'),
|
||||
colored($yv_utils->short_human_number($yv_utils->get_video_count($playlist)), 'yellow'),
|
||||
);
|
||||
}
|
||||
elsif ($opt{results_fixed_width}) {
|
||||
|
||||
require List::Util;
|
||||
|
||||
my @authors = map { $yv_utils->get_channel_title($_) } @{$playlists};
|
||||
my @dates = map { $yv_utils->get_publication_date($_) } @{$playlists};
|
||||
my @authors = map { $yv_utils->get_channel_title($_) } @{$playlists};
|
||||
my @dates = map { $yv_utils->get_publication_date($_) } @{$playlists};
|
||||
my @video_counts = map { $yv_utils->short_human_number($yv_utils->get_video_count($_)) } @{$playlists};
|
||||
|
||||
my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
|
||||
my $dates_width = List::Util::max(map { length($_) } @dates);
|
||||
my $title_length = $term_width - ($author_width + $dates_width + 2 + 3 + 1 + 2);
|
||||
my $author_width = List::Util::min(List::Util::max(map { length($_) } @authors), int($term_width / 5));
|
||||
my $dates_width = List::Util::max(map { length($_) } @dates);
|
||||
my $video_count_width = List::Util::max(map { length($_) } @video_counts);
|
||||
my $title_length = $term_width - ($author_width + $dates_width + $video_count_width + 2 + 3 + 1 + 2 + 1 + 2);
|
||||
|
||||
print "\n";
|
||||
foreach my $i (0 .. $#{$playlists}) {
|
||||
@@ -2804,19 +2861,23 @@ FORMAT
|
||||
my $playlist = $playlists->[$i];
|
||||
my $title = clear_title($yv_utils->get_title($playlist));
|
||||
|
||||
printf "%s. %s %s [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
printf "%s. %s %s [%*s] [%*s]\n", colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
adjust_width($title, $title_length),
|
||||
adjust_width($authors[$i], $author_width, 1),
|
||||
$dates_width, $dates[$i];
|
||||
$dates_width, $dates[$i],
|
||||
$video_count_width, $video_counts[$i];
|
||||
}
|
||||
last;
|
||||
}
|
||||
else {
|
||||
print "\n" if $i == 0;
|
||||
printf(
|
||||
"%s. %s (by %s) [%s]\n",
|
||||
colored(sprintf('%2d', $i + 1), 'bold'), $yv_utils->get_title($playlist),
|
||||
$yv_utils->get_channel_title($playlist), $yv_utils->get_publication_date($playlist)
|
||||
"%s. %s (by %s) [%s] [%s]\n",
|
||||
colored(sprintf('%2d', $i + 1), 'bold'),
|
||||
$yv_utils->get_title($playlist),
|
||||
$yv_utils->get_channel_title($playlist),
|
||||
$yv_utils->get_publication_date($playlist),
|
||||
$yv_utils->short_human_number($yv_utils->get_video_count($playlist)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2867,6 +2928,7 @@ FORMAT
|
||||
);
|
||||
$desc =~ s/^\s+//;
|
||||
printf $info_format, $yv_utils->get_title($playlists->[$id]),
|
||||
$yv_utils->short_human_number($yv_utils->get_video_count($playlists->[$id])),
|
||||
($yv_utils->get_playlist_id($playlists->[$id])) x 2, $desc;
|
||||
}
|
||||
press_enter_to_continue();
|
||||
@@ -2965,27 +3027,27 @@ sub get_streaming_url {
|
||||
if (ref($captions) eq 'ARRAY' and @$captions and $opt{get_captions} and not $opt{novideo}) {
|
||||
require WWW::PipeViewer::GetCaption;
|
||||
my $yv_cap = WWW::PipeViewer::GetCaption->new(
|
||||
auto_captions => $opt{auto_captions},
|
||||
captions_dir => $opt{cache_dir},
|
||||
captions => $captions,
|
||||
languages => $CONFIG{srt_languages},
|
||||
yv_obj => $yv_obj,
|
||||
);
|
||||
auto_captions => $opt{auto_captions},
|
||||
captions_dir => $opt{cache_dir},
|
||||
captions => $captions,
|
||||
languages => $CONFIG{srt_languages},
|
||||
yv_obj => $yv_obj,
|
||||
);
|
||||
$srt_file = $yv_cap->save_caption($video_id);
|
||||
}
|
||||
|
||||
require WWW::PipeViewer::Itags;
|
||||
state $yv_itags = WWW::PipeViewer::Itags->new();
|
||||
|
||||
# Include DASH itags
|
||||
my $dash = 1;
|
||||
# Include split-videos
|
||||
my $split_videos = 1;
|
||||
|
||||
# Exclude DASH itags in download-mode or when no video output is required
|
||||
if ($opt{novideo} or not $opt{dash_support}) {
|
||||
$dash = 0;
|
||||
# Exclude split-videos in download-mode or when no video output is required
|
||||
if ($opt{novideo} or not $opt{split_videos}) {
|
||||
$split_videos = 0;
|
||||
}
|
||||
elsif ($opt{download_video}) {
|
||||
$dash = $opt{merge_into_mkv} ? 1 : 0;
|
||||
$split_videos = $opt{merge_into_mkv} ? 1 : 0;
|
||||
}
|
||||
|
||||
my ($streaming, $resolution) = $yv_itags->find_streaming_url(
|
||||
@@ -2995,9 +3057,9 @@ sub get_streaming_url {
|
||||
hfr => $opt{hfr},
|
||||
ignore_av1 => $opt{ignore_av1},
|
||||
|
||||
dash => $dash,
|
||||
dash_mp4_audio => ($opt{novideo} ? 1 : $opt{dash_mp4_audio}),
|
||||
dash_segmented => ($opt{download_video} ? 0 : $opt{dash_segmented}),
|
||||
split => $split_videos,
|
||||
m4a_audio => ($opt{novideo} ? 1 : $opt{m4a_audio}),
|
||||
dash => ($opt{download_video} ? 0 : $opt{dash}),
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -3293,7 +3355,7 @@ sub get_player_command {
|
||||
)
|
||||
);
|
||||
|
||||
my $has_video = $cmd =~ /\*(?:VIDEO|URL|ID)\*/;
|
||||
my $has_video = $cmd =~ /(?:^|\s)\*(?:VIDEO|URL)\*(?:\s|\z)/;
|
||||
|
||||
$cmd = $yv_utils->format_text(
|
||||
streaming => $streaming,
|
||||
@@ -3530,6 +3592,16 @@ sub print_videos {
|
||||
$videos = $videos->{videos};
|
||||
}
|
||||
|
||||
if (ref($videos) eq 'HASH' and exists $videos->{entries}) {
|
||||
$videos = $videos->{entries};
|
||||
}
|
||||
|
||||
my $token = undef;
|
||||
|
||||
if (ref($results->{results}) eq 'HASH' and exists $results->{results}{continuation}) {
|
||||
$token = $results->{results}{continuation};
|
||||
}
|
||||
|
||||
if (ref($videos) ne 'ARRAY') {
|
||||
|
||||
my $current_instance = $yv_obj->get_api_host();
|
||||
@@ -3757,7 +3829,7 @@ sub print_videos {
|
||||
}
|
||||
else {
|
||||
if (defined($url)) {
|
||||
__SUB__->($yv_obj->next_page($url), auto => 1);
|
||||
__SUB__->($yv_obj->next_page($url, $token), auto => 1);
|
||||
}
|
||||
else {
|
||||
$opt{play_all} = 0;
|
||||
@@ -3812,7 +3884,7 @@ sub print_videos {
|
||||
}
|
||||
elsif ($opt =~ /^(?:n|next)\z/) {
|
||||
if (defined($url)) {
|
||||
my $request = $yv_obj->next_page($url);
|
||||
my $request = $yv_obj->next_page($url, $token);
|
||||
__SUB__->($request, @keywords ? (auto => 1) : ());
|
||||
}
|
||||
else {
|
||||
@@ -4213,18 +4285,10 @@ The special tokens for C<text> are listed in:
|
||||
|
||||
pipe-viewer --tricks
|
||||
|
||||
=head2 dash_mp4_audio
|
||||
|
||||
Include or exclude MP4/M4A (AAC) audio files.
|
||||
|
||||
=head2 dash_segmented
|
||||
=head2 dash
|
||||
|
||||
Include or exclude streams in "Dynamic Adaptive Streaming over HTTP" (DASH) format.
|
||||
|
||||
=head2 dash_support
|
||||
|
||||
Enable or disable support for split videos.
|
||||
|
||||
=head2 date
|
||||
|
||||
Search for videos uploaded within a specific amount of time.
|
||||
@@ -4347,6 +4411,10 @@ Arguments for C<ffmpeg> how to merge the files.
|
||||
|
||||
Include closed-captions inside the MKV container (if any).
|
||||
|
||||
=head2 m4a_audio
|
||||
|
||||
When set to C<0>, MP4/M4A (AAC) audio files will be ignored.
|
||||
|
||||
=head2 order
|
||||
|
||||
Search order for videos.
|
||||
@@ -4411,6 +4479,10 @@ When downloading, skip if the file already exists locally.
|
||||
|
||||
Skip already watched/downloaded videos.
|
||||
|
||||
=head2 split_videos
|
||||
|
||||
Enable or disable support for split-videos. "Split-videos" are videos that do not include audio and video in the same file.
|
||||
|
||||
=head2 srt_languages
|
||||
|
||||
List of SRT languages in the order of preference.
|
||||
|
||||
+60
-16
@@ -12,6 +12,7 @@ memoize('_extract_from_ytdl');
|
||||
memoize('_extract_from_invidious');
|
||||
|
||||
use parent qw(
|
||||
WWW::PipeViewer::InitialData
|
||||
WWW::PipeViewer::Search
|
||||
WWW::PipeViewer::Videos
|
||||
WWW::PipeViewer::Channels
|
||||
@@ -31,7 +32,7 @@ WWW::PipeViewer - A very easy interface to YouTube, using the API of invidio.us.
|
||||
|
||||
=cut
|
||||
|
||||
our $VERSION = '0.0.1';
|
||||
our $VERSION = '0.0.4';
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
@@ -105,7 +106,8 @@ my %valid_options = (
|
||||
|
||||
#<<<
|
||||
# LWP user agent
|
||||
user_agent => {valid => qr/^.{5}/, default => 'Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53'},
|
||||
#user_agent => {valid => qr/^.{5}/, default => 'Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53'},
|
||||
user_agent => {valid => qr/^.{5}/, default => 'Mozilla/5.0 (Android 10; Tablet; rv:82.0) Gecko/82.0 Firefox/82.0,gzip(gfe)'},
|
||||
#>>>
|
||||
);
|
||||
|
||||
@@ -278,8 +280,8 @@ sub set_lwp_useragent {
|
||||
$code >= 300 # do not cache any bad response
|
||||
or $response->request->method ne 'GET' # cache only GET requests
|
||||
|
||||
# don't cache if "cache-control" specifies "max-age=0" or "no-store"
|
||||
or (($response->header('cache-control') // '') =~ /\b(?:max-age=0|no-store)\b/)
|
||||
# don't cache if "cache-control" specifies "max-age=0", "no-store" or "no-cache"
|
||||
or (($response->header('cache-control') // '') =~ /\b(?:max-age=0|no-store|no-cache)\b/)
|
||||
|
||||
# don't cache video or audio files
|
||||
or (($response->header('content-type') // '') =~ /\b(?:video|audio)\b/);
|
||||
@@ -549,7 +551,7 @@ sub get_invidious_instances {
|
||||
}
|
||||
|
||||
sub select_good_invidious_instances {
|
||||
my ($self) = @_;
|
||||
my ($self, %args) = @_;
|
||||
|
||||
state $instances = $self->get_invidious_instances;
|
||||
|
||||
@@ -557,19 +559,25 @@ sub select_good_invidious_instances {
|
||||
|
||||
my %ignored = (
|
||||
'yewtu.be' => 1,
|
||||
'invidiou.site' => 1,
|
||||
'invidious.tube' => 1,
|
||||
'invidiou.site' => 0,
|
||||
'invidious.xyz' => 1,
|
||||
'vid.mint.lgbt' => 1,
|
||||
'invidious.ggc-project.de' => 1,
|
||||
'invidious.toot.koeln' => 1,
|
||||
'invidious.kavin.rocks' => 1,
|
||||
'invidious.kavin.rocks' => 0,
|
||||
'invidious.snopyta.org' => 0,
|
||||
);
|
||||
|
||||
#<<<
|
||||
my @candidates =
|
||||
grep { not $ignored{$_->[0]} }
|
||||
grep { ref($_->[1]{monitor}) eq 'HASH' ? ($_->[1]{monitor}{statusClass} eq 'success') : 1 }
|
||||
grep { $args{lax} ? 1 : eval { lc($_->[1]{monitor}{dailyRatios}[0]{label} // '') eq 'success' } }
|
||||
#~ grep { $args{lax} ? 1 : eval { lc($_->[1]{monitor}{weeklyRatio}{label} // '') eq 'success' } }
|
||||
grep { $args{lax} ? 1 : eval { lc($_->[1]{monitor}{statusClass} // '') eq 'success' } }
|
||||
#~ grep { $args{lax} ? 1 : !exists($_->[1]{stats}{error}) }
|
||||
grep { lc($_->[1]{type} // '') eq 'https' } @$instances;
|
||||
#>>>
|
||||
|
||||
if ($self->get_debug) {
|
||||
|
||||
@@ -584,7 +592,15 @@ sub select_good_invidious_instances {
|
||||
|
||||
sub pick_random_instance {
|
||||
my ($self) = @_;
|
||||
|
||||
# TODO: make sure the selected invidious instance actually works.
|
||||
|
||||
my @candidates = $self->select_good_invidious_instances();
|
||||
|
||||
if (not @candidates) {
|
||||
@candidates = $self->select_good_invidious_instances(lax => 1);
|
||||
}
|
||||
|
||||
$candidates[rand @candidates];
|
||||
}
|
||||
|
||||
@@ -675,11 +691,20 @@ sub _make_feed_url {
|
||||
sub _extract_from_invidious {
|
||||
my ($self, $videoID) = @_;
|
||||
|
||||
my @instances = $self->select_good_invidious_instances();
|
||||
my @candidates = $self->select_good_invidious_instances();
|
||||
my @extra_candidates = $self->select_good_invidious_instances(lax => 1);
|
||||
|
||||
require List::Util;
|
||||
|
||||
#<<<
|
||||
my %seen;
|
||||
my @instances = grep { !$seen{$_}++ } (
|
||||
List::Util::shuffle(map { $_->[0] } @candidates),
|
||||
List::Util::shuffle(map { $_->[0] } @extra_candidates),
|
||||
);
|
||||
#>>>
|
||||
|
||||
if (@instances) {
|
||||
require List::Util;
|
||||
@instances = List::Util::shuffle(map { $_->[0] } @instances);
|
||||
push @instances, 'invidious.snopyta.org';
|
||||
}
|
||||
else {
|
||||
@@ -988,11 +1013,6 @@ sub _extract_streaming_urls {
|
||||
|
||||
$self->_check_streaming_urls($videoID, \@results);
|
||||
|
||||
if (grep { $_->{url} =~ /\bsc=yes\b/ } @results) {
|
||||
say STDERR ":: Contains SC = yes" if $self->get_debug;
|
||||
##return;
|
||||
}
|
||||
|
||||
# Keep only streams with contentLength > 0.
|
||||
@results = grep { $_->{itag} == 22 or (exists($_->{contentLength}) and $_->{contentLength} > 0) } @results;
|
||||
|
||||
@@ -1176,6 +1196,22 @@ sub post_as_json {
|
||||
sub next_page_with_token {
|
||||
my ($self, $url, $token) = @_;
|
||||
|
||||
if ($token =~ /^ytsearch:(\w+):(.*)/) {
|
||||
return $self->yt_search_next_page($url, $2, type => $1, url => $url);
|
||||
}
|
||||
|
||||
if ($token =~ /^ytplaylist:(\w+):(.*)/) {
|
||||
return $self->yt_playlist_next_page($url, $2, type => $1, url => $url);
|
||||
}
|
||||
|
||||
if ($url =~ m{^https://m\.youtube\.com}) {
|
||||
return
|
||||
scalar {
|
||||
url => $url,
|
||||
results => [],
|
||||
};
|
||||
}
|
||||
|
||||
if (not $url =~ s{[?&]continuation=\K([^&]+)}{$token}) {
|
||||
$url = $self->_append_url_args($url, continuation => $token);
|
||||
}
|
||||
@@ -1192,6 +1228,14 @@ sub next_page {
|
||||
return $self->next_page_with_token($url, $token);
|
||||
}
|
||||
|
||||
if ($url =~ m{^https://m\.youtube\.com}) {
|
||||
return
|
||||
scalar {
|
||||
url => $url,
|
||||
results => [],
|
||||
};
|
||||
}
|
||||
|
||||
if (not $url =~ s{[?&]page=\K(\d+)}{$1+1}e) {
|
||||
$url = $self->_append_url_args($url, page => 2);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ sub oauth_refresh_token {
|
||||
my $json_data = $self->lwp_post(
|
||||
$self->_get_token_oauth_url(),
|
||||
[Content => $self->get_www_content_type,
|
||||
client_id => $self->get_client_id() // return,
|
||||
client_id => $self->get_client_id() // return,
|
||||
client_secret => $self->get_client_secret() // return,
|
||||
refresh_token => $self->get_refresh_token() // return,
|
||||
grant_type => 'refresh_token',
|
||||
@@ -56,7 +56,7 @@ sub get_accounts_oauth_url {
|
||||
my $url = $self->_append_url_args(
|
||||
($self->get_oauth_url() . 'auth'),
|
||||
response_type => 'code',
|
||||
client_id => $self->get_client_id() // return,
|
||||
client_id => $self->get_client_id() // return,
|
||||
redirect_uri => $self->get_redirect_uri() // return,
|
||||
scope => 'https://www.googleapis.com/auth/youtube.force-ssl',
|
||||
access_type => 'offline',
|
||||
@@ -80,9 +80,9 @@ sub oauth_login {
|
||||
my $json_data = $self->lwp_post(
|
||||
$self->_get_token_oauth_url(),
|
||||
[Content => $self->get_www_content_type,
|
||||
client_id => $self->get_client_id() // return,
|
||||
client_id => $self->get_client_id() // return,
|
||||
client_secret => $self->get_client_secret() // return,
|
||||
redirect_uri => $self->get_redirect_uri() // return,
|
||||
redirect_uri => $self->get_redirect_uri() // return,
|
||||
grant_type => 'authorization_code',
|
||||
code => $code,
|
||||
]
|
||||
|
||||
@@ -25,12 +25,18 @@ sub _make_channels_url {
|
||||
|
||||
sub videos_from_channel_id {
|
||||
my ($self, $channel_id) = @_;
|
||||
return $self->_get_results($self->_make_feed_url("channels/$channel_id/videos"));
|
||||
|
||||
if (my $results = $self->yt_channel_uploads($channel_id)) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
my $url = $self->_make_feed_url("channels/$channel_id/videos");
|
||||
return $self->_get_results($url);
|
||||
}
|
||||
|
||||
sub videos_from_username {
|
||||
my ($self, $channel_id) = @_;
|
||||
return $self->_get_results($self->_make_feed_url("channels/$channel_id/videos"));
|
||||
$self->videos_from_channel_id($channel_id);
|
||||
}
|
||||
|
||||
=head2 popular_videos($channel_id)
|
||||
@@ -46,7 +52,12 @@ sub popular_videos {
|
||||
return $self->_get_results($self->_make_feed_url('popular'));
|
||||
}
|
||||
|
||||
return $self->_get_results($self->_make_feed_url("channels/$channel_id/videos", sort_by => 'popular'));
|
||||
if (my $results = $self->yt_channel_uploads($channel_id, sort_by => 'popular')) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
my $url = $self->_make_feed_url("channels/$channel_id/videos", sort_by => 'popular');
|
||||
return $self->_get_results($url);
|
||||
}
|
||||
|
||||
=head2 channels_from_categoryID($category_id)
|
||||
|
||||
@@ -36,11 +36,7 @@ Retrieve comments from a video ID.
|
||||
|
||||
sub comments_from_video_id {
|
||||
my ($self, $video_id) = @_;
|
||||
$self->_get_results(
|
||||
$self->_make_feed_url("comments/$video_id",
|
||||
sort_by => $self->get_comments_order,
|
||||
),
|
||||
);
|
||||
$self->_get_results($self->_make_feed_url("comments/$video_id", sort_by => $self->get_comments_order));
|
||||
}
|
||||
|
||||
=head2 comment_to_video_id($comment, $videoID)
|
||||
|
||||
@@ -0,0 +1,882 @@
|
||||
package WWW::PipeViewer::InitialData;
|
||||
|
||||
use utf8;
|
||||
use 5.014;
|
||||
use warnings;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
WWW::PipeViewer::InitialData - Extract initial data.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use WWW::PipeViewer;
|
||||
my $obj = WWW::PipeViewer->new(%opts);
|
||||
|
||||
my $results = $obj->yt_search(q => $keywords);
|
||||
my $playlists = $obj->yt_channel_playlists($channel_ID);
|
||||
|
||||
=head1 SUBROUTINES/METHODS
|
||||
|
||||
=cut
|
||||
|
||||
sub _time_to_seconds {
|
||||
my ($time) = @_;
|
||||
|
||||
my ($hours, $minutes, $seconds) = (0, 0, 0);
|
||||
|
||||
if ($time =~ /(\d+):(\d+):(\d+)/) {
|
||||
($hours, $minutes, $seconds) = ($1, $2, $3);
|
||||
}
|
||||
elsif ($time =~ /(\d+):(\d+)/) {
|
||||
($minutes, $seconds) = ($1, $2);
|
||||
}
|
||||
elsif ($time =~ /(\d+)/) {
|
||||
$seconds = $1;
|
||||
}
|
||||
|
||||
$hours * 3600 + $minutes * 60 + $seconds;
|
||||
}
|
||||
|
||||
sub _human_number_to_int {
|
||||
my ($text) = @_;
|
||||
|
||||
# 7.6K -> 7600; 7.6M -> 7600000
|
||||
if ($text =~ /([\d,.]+)\s*([KMB])/i) {
|
||||
|
||||
my $v = $1;
|
||||
my $u = $2;
|
||||
my $m = ($u eq 'K' ? 1e3 : ($u eq 'M' ? 1e6 : ($u eq 'B' ? 1e9 : 1)));
|
||||
|
||||
$v =~ tr/,/./;
|
||||
|
||||
return int($v * $m);
|
||||
}
|
||||
|
||||
if ($text =~ /([\d,.]+)/) {
|
||||
my $v = $1;
|
||||
$v =~ tr/,.//d;
|
||||
return int($v);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _thumbnail_quality {
|
||||
my ($width) = @_;
|
||||
|
||||
$width // return 'medium';
|
||||
|
||||
if ($width == 1280) {
|
||||
return "maxres";
|
||||
}
|
||||
|
||||
if ($width == 640) {
|
||||
return "sddefault";
|
||||
}
|
||||
|
||||
if ($width == 480) {
|
||||
return 'high';
|
||||
}
|
||||
|
||||
if ($width == 320) {
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
if ($width == 120) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if ($width <= 120) {
|
||||
return 'small';
|
||||
}
|
||||
|
||||
if ($width <= 176) {
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
if ($width <= 480) {
|
||||
return 'high';
|
||||
}
|
||||
|
||||
if ($width <= 640) {
|
||||
return 'sddefault';
|
||||
}
|
||||
|
||||
if ($width <= 1280) {
|
||||
return "maxres";
|
||||
}
|
||||
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
sub _fix_url_protocol {
|
||||
my ($url) = @_;
|
||||
|
||||
$url // return undef;
|
||||
|
||||
if ($url =~ m{^https://}) { # ok
|
||||
return $url;
|
||||
}
|
||||
if ($url =~ s{^.*?//}{}) {
|
||||
return "https://" . $url;
|
||||
}
|
||||
if ($url =~ /^\w+\./) {
|
||||
return "https://" . $url;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
sub _unscramble {
|
||||
my ($str) = @_;
|
||||
|
||||
my $i = my $l = length($str);
|
||||
|
||||
$str =~ s/(.)(.{$i})/$2$1/sg while (--$i > 0);
|
||||
$str =~ s/(.)(.{$i})/$2$1/sg while (++$i < $l);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
sub _extract_youtube_mix {
|
||||
my ($self, $data) = @_;
|
||||
|
||||
my $info = eval { $data->{callToAction}{watchCardHeroVideoRenderer} } || return;
|
||||
my $header = eval { $data->{header}{watchCardRichHeaderRenderer} };
|
||||
|
||||
my %mix;
|
||||
|
||||
$mix{type} = 'playlist';
|
||||
|
||||
$mix{title} =
|
||||
eval { $header->{title}{runs}[0]{text} }
|
||||
// eval { $info->{accessibility}{accessibilityData}{label} }
|
||||
// eval { $info->{callToActionButton}{callToActionButtonRenderer}{label}{runs}[0]{text} } // 'Youtube Mix';
|
||||
|
||||
$mix{playlistId} = eval { $info->{navigationEndpoint}{watchEndpoint}{playlistId} } || return;
|
||||
|
||||
$mix{playlistThumbnail} = eval { _fix_url_protocol($header->{avatar}{thumbnails}[0]{url}) }
|
||||
// eval { _fix_url_protocol($info->{heroImage}{collageHeroImageRenderer}{leftThumbnail}{thumbnails}[0]{url}) };
|
||||
|
||||
$mix{description} = _extract_description({title => $info});
|
||||
|
||||
$mix{author} = eval { $header->{title}{runs}[0]{text} } // "YouTube";
|
||||
$mix{authorId} = eval { $header->{titleNavigationEndpoint}{browseEndpoint}{browseId} } // "youtube";
|
||||
|
||||
return \%mix;
|
||||
}
|
||||
|
||||
sub _extract_author_name {
|
||||
my ($info) = @_;
|
||||
eval { $info->{longBylineText}{runs}[0]{text} } // eval { $info->{shortBylineText}{runs}[0]{text} };
|
||||
}
|
||||
|
||||
sub _extract_video_id {
|
||||
my ($info) = @_;
|
||||
eval { $info->{videoId} } || eval { $info->{navigationEndpoint}{watchEndpoint}{videoId} } || undef;
|
||||
}
|
||||
|
||||
sub _extract_length_seconds {
|
||||
my ($info) = @_;
|
||||
eval { $info->{lengthSeconds} }
|
||||
|| _time_to_seconds(eval { $info->{thumbnailOverlays}[0]{thumbnailOverlayTimeStatusRenderer}{text}{runs}[0]{text} } // 0)
|
||||
|| _time_to_seconds(eval { $info->{lengthText}{runs}[0]{text} // 0 });
|
||||
}
|
||||
|
||||
sub _extract_published_text {
|
||||
my ($info) = @_;
|
||||
|
||||
my $text = eval { $info->{publishedTimeText}{runs}[0]{text} } || return undef;
|
||||
|
||||
if ($text =~ /(\d+)\s+(\w+)/) {
|
||||
return "$1 $2 ago";
|
||||
}
|
||||
|
||||
if ($text =~ /(\d+)\s*(\w+)/) {
|
||||
return "$1 $2 ago";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
sub _extract_channel_id {
|
||||
my ($info) = @_;
|
||||
eval { $info->{channelId} }
|
||||
// eval { $info->{shortBylineText}{runs}[0]{navigationEndpoint}{browseEndpoint}{browseId} }
|
||||
// eval { $info->{navigationEndpoint}{browseEndpoint}{browseId} };
|
||||
}
|
||||
|
||||
sub _extract_view_count_text {
|
||||
my ($info) = @_;
|
||||
eval { $info->{shortViewCountText}{runs}[0]{text} };
|
||||
}
|
||||
|
||||
sub _extract_thumbnails {
|
||||
my ($info) = @_;
|
||||
eval {
|
||||
[
|
||||
map {
|
||||
my %thumb = %$_;
|
||||
$thumb{quality} = _thumbnail_quality($thumb{width});
|
||||
$thumb{url} = _fix_url_protocol($thumb{url});
|
||||
\%thumb;
|
||||
} @{$info->{thumbnail}{thumbnails}}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
sub _extract_playlist_thumbnail {
|
||||
my ($info) = @_;
|
||||
eval {
|
||||
_fix_url_protocol(
|
||||
(
|
||||
grep { _thumbnail_quality($_->{width}) =~ /medium|high/ }
|
||||
@{$info->{thumbnailRenderer}{playlistVideoThumbnailRenderer}{thumbnail}{thumbnails}}
|
||||
)[0]{url} // $info->{thumbnailRenderer}{playlistVideoThumbnailRenderer}{thumbnail}{thumbnails}[0]{url}
|
||||
);
|
||||
} // eval {
|
||||
_fix_url_protocol((grep { _thumbnail_quality($_->{width}) =~ /medium|high/ } @{$info->{thumbnail}{thumbnails}})[0]{url}
|
||||
// $info->{thumbnail}{thumbnails}[0]{url});
|
||||
};
|
||||
}
|
||||
|
||||
sub _extract_title {
|
||||
my ($info) = @_;
|
||||
eval { $info->{title}{runs}[0]{text} } // eval { $info->{title}{accessibility}{accessibilityData}{label} };
|
||||
}
|
||||
|
||||
sub _extract_description {
|
||||
my ($info) = @_;
|
||||
|
||||
# FIXME: this is not the video description
|
||||
eval { $info->{title}{accessibility}{accessibilityData}{label} };
|
||||
}
|
||||
|
||||
sub _extract_view_count {
|
||||
my ($info) = @_;
|
||||
_human_number_to_int(eval { $info->{viewCountText}{runs}[0]{text} } || 0);
|
||||
}
|
||||
|
||||
sub _extract_video_count {
|
||||
my ($info) = @_;
|
||||
_human_number_to_int( eval { $info->{videoCountShortText}{runs}[0]{text} }
|
||||
|| eval { $info->{videoCountText}{runs}[0]{text} }
|
||||
|| 0);
|
||||
}
|
||||
|
||||
sub _extract_subscriber_count {
|
||||
my ($info) = @_;
|
||||
_human_number_to_int(eval { $info->{subscriberCountText}{runs}[0]{text} } || 0);
|
||||
}
|
||||
|
||||
sub _extract_playlist_id {
|
||||
my ($info) = @_;
|
||||
eval { $info->{playlistId} };
|
||||
}
|
||||
|
||||
sub _extract_itemSection_entry {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
ref($data) eq 'HASH' or return;
|
||||
|
||||
# Album
|
||||
if ($args{type} eq 'all' and exists $data->{horizontalCardListRenderer}) { # TODO
|
||||
return;
|
||||
}
|
||||
|
||||
# Video
|
||||
if (exists($data->{compactVideoRenderer}) or exists($data->{playlistVideoRenderer})) {
|
||||
|
||||
my %video;
|
||||
my $info = $data->{compactVideoRenderer} // $data->{playlistVideoRenderer};
|
||||
|
||||
$video{type} = 'video';
|
||||
|
||||
# Deleted video
|
||||
if (defined(eval { $info->{isPlayable} }) and not $info->{isPlayable}) {
|
||||
return;
|
||||
}
|
||||
|
||||
$video{videoId} = _extract_video_id($info) // return;
|
||||
$video{title} = _extract_title($info) // return;
|
||||
$video{lengthSeconds} = _extract_length_seconds($info) || 0;
|
||||
$video{liveNow} = ($video{lengthSeconds} == 0);
|
||||
$video{author} = _extract_author_name($info) // return;
|
||||
$video{authorId} = _extract_channel_id($info) // return;
|
||||
$video{publishedText} = _extract_published_text($info);
|
||||
$video{viewCountText} = _extract_view_count_text($info);
|
||||
$video{videoThumbnails} = _extract_thumbnails($info);
|
||||
$video{description} = _extract_description($info);
|
||||
$video{viewCount} = _extract_view_count($info);
|
||||
|
||||
return \%video;
|
||||
}
|
||||
|
||||
# Playlist
|
||||
if ($args{type} ne 'video' and exists $data->{compactPlaylistRenderer}) {
|
||||
|
||||
my %playlist;
|
||||
my $info = $data->{compactPlaylistRenderer};
|
||||
|
||||
$playlist{type} = 'playlist';
|
||||
|
||||
$playlist{title} = _extract_title($info) // return;
|
||||
$playlist{playlistId} = _extract_playlist_id($info) // return;
|
||||
$playlist{author} = _extract_author_name($info);
|
||||
$playlist{authorId} = _extract_channel_id($info);
|
||||
$playlist{videoCount} = _extract_video_count($info);
|
||||
$playlist{playlistThumbnail} = _extract_playlist_thumbnail($info);
|
||||
$playlist{description} = _extract_description($info);
|
||||
|
||||
return \%playlist;
|
||||
}
|
||||
|
||||
# Channel
|
||||
if ($args{type} ne 'video' and exists $data->{compactChannelRenderer}) {
|
||||
|
||||
my %channel;
|
||||
my $info = $data->{compactChannelRenderer};
|
||||
|
||||
$channel{type} = 'channel';
|
||||
|
||||
$channel{author} = _extract_title($info) // return;
|
||||
$channel{authorId} = _extract_channel_id($info) // return;
|
||||
$channel{subCount} = _extract_subscriber_count($info);
|
||||
$channel{videoCount} = _extract_video_count($info);
|
||||
$channel{authorThumbnails} = _extract_thumbnails($info);
|
||||
$channel{description} = _extract_description($info);
|
||||
|
||||
return \%channel;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub _parse_itemSection {
|
||||
my ($self, $entry, %args) = @_;
|
||||
|
||||
eval { ref($entry->{contents}) eq 'ARRAY' } || return;
|
||||
|
||||
my @results;
|
||||
|
||||
foreach my $entry (@{$entry->{contents}}) {
|
||||
|
||||
my $item = $self->_extract_itemSection_entry($entry, %args);
|
||||
|
||||
if (defined($item) and ref($item) eq 'HASH') {
|
||||
push @results, $item;
|
||||
}
|
||||
}
|
||||
|
||||
if (exists($entry->{continuations}) and ref($entry->{continuations}) eq 'ARRAY') {
|
||||
|
||||
my $token = eval { $entry->{continuations}[0]{nextContinuationData}{continuation} };
|
||||
|
||||
if (defined($token)) {
|
||||
push @results,
|
||||
scalar {
|
||||
type => 'nextpage',
|
||||
token => "ytplaylist:$args{type}:$token",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return @results;
|
||||
}
|
||||
|
||||
sub _extract_sectionList_results {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
eval { ref($data->{contents}) eq 'ARRAY' } or return;
|
||||
|
||||
my @results;
|
||||
|
||||
foreach my $entry (@{$data->{contents}}) {
|
||||
|
||||
# Playlists
|
||||
if (eval { ref($entry->{shelfRenderer}{content}{verticalListRenderer}{items}) eq 'ARRAY' }) {
|
||||
push @results,
|
||||
$self->_parse_itemSection({contents => $entry->{shelfRenderer}{content}{verticalListRenderer}{items}}, %args);
|
||||
}
|
||||
|
||||
# Playlist videos
|
||||
if (eval { ref($entry->{itemSectionRenderer}{contents}[0]{playlistVideoListRenderer}{contents}) eq 'ARRAY' }) {
|
||||
push @results,
|
||||
$self->_parse_itemSection($entry->{itemSectionRenderer}{contents}[0]{playlistVideoListRenderer}, %args);
|
||||
next;
|
||||
}
|
||||
|
||||
# YouTube Mix
|
||||
if ($args{type} eq 'all' and exists $entry->{universalWatchCardRenderer}) {
|
||||
|
||||
my $mix = $self->_extract_youtube_mix($entry->{universalWatchCardRenderer});
|
||||
|
||||
if (defined($mix)) {
|
||||
push(@results, $mix);
|
||||
}
|
||||
}
|
||||
|
||||
# Video results
|
||||
if (exists $entry->{itemSectionRenderer}) {
|
||||
push @results, $self->_parse_itemSection($entry->{itemSectionRenderer}, %args);
|
||||
}
|
||||
|
||||
# Continuation page
|
||||
if (exists $entry->{continuationItemRenderer}) {
|
||||
|
||||
my $info = $entry->{continuationItemRenderer};
|
||||
my $token = eval { $info->{continuationEndpoint}{continuationCommand}{token} };
|
||||
|
||||
if (defined($token)) {
|
||||
push @results,
|
||||
scalar {
|
||||
type => 'nextpage',
|
||||
token => "ytsearch:$args{type}:$token",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (@results and exists $data->{continuations}) {
|
||||
push @results, $self->_parse_itemSection($data, %args);
|
||||
}
|
||||
|
||||
return @results;
|
||||
}
|
||||
|
||||
sub _add_author_to_results {
|
||||
my ($self, $data, $results, %args) = @_;
|
||||
|
||||
my $header = eval { $data->{header}{c4TabbedHeaderRenderer} } // eval { $data->{metadata}{channelMetadataRenderer} };
|
||||
|
||||
my $channel_id = eval { $header->{channelId} } // eval { $header->{externalId} };
|
||||
my $channel_name = eval { $header->{title} };
|
||||
|
||||
foreach my $result (@$results) {
|
||||
if (ref($result) eq 'HASH') {
|
||||
$result->{author} = $channel_name if defined($channel_name);
|
||||
$result->{authorId} = $channel_id if defined($channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub _find_sectionList {
|
||||
my ($self, $data) = @_;
|
||||
|
||||
eval {
|
||||
(
|
||||
grep {
|
||||
eval { exists($_->{tabRenderer}{content}{sectionListRenderer}{contents}) }
|
||||
} @{$data->{contents}{singleColumnBrowseResultsRenderer}{tabs}}
|
||||
)[0]{tabRenderer}{content}{sectionListRenderer};
|
||||
} // undef;
|
||||
}
|
||||
|
||||
sub _extract_channel_uploads {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
my @results = $self->_extract_sectionList_results($self->_find_sectionList($data), %args);
|
||||
$self->_add_author_to_results($data, \@results, %args);
|
||||
return @results;
|
||||
}
|
||||
|
||||
sub _extract_channel_playlists {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
my @results = $self->_extract_sectionList_results($self->_find_sectionList($data), %args);
|
||||
$self->_add_author_to_results($data, \@results, %args);
|
||||
return @results;
|
||||
}
|
||||
|
||||
sub _extract_playlist_videos {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
my @results = $self->_extract_sectionList_results($self->_find_sectionList($data), %args);
|
||||
$self->_add_author_to_results($data, \@results, %args);
|
||||
return @results;
|
||||
}
|
||||
|
||||
sub _get_initial_data {
|
||||
my ($self, $url) = @_;
|
||||
|
||||
my $content = $self->lwp_get($url) // return;
|
||||
|
||||
if ($content =~ m{<div id="initial-data"><!--(.*?)--></div>}is) {
|
||||
my $json = $1;
|
||||
my $hash = $self->parse_utf8_json_string($json);
|
||||
return $hash;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub _channel_data {
|
||||
my ($self, $channel, %args) = @_;
|
||||
|
||||
state $yv_utils = WWW::PipeViewer::Utils->new();
|
||||
|
||||
my $url = $self->get_m_youtube_url;
|
||||
|
||||
if ($yv_utils->is_channelID($channel)) {
|
||||
$url .= "/channel/$channel/$args{type}";
|
||||
}
|
||||
else {
|
||||
$url .= "/c/$channel/$args{type}";
|
||||
}
|
||||
|
||||
my %params = (hl => "en");
|
||||
|
||||
if (defined(my $sort = $args{sort_by})) {
|
||||
if ($sort eq 'popular') {
|
||||
$params{sort} = 'p';
|
||||
}
|
||||
elsif ($sort eq 'old') {
|
||||
$params{sort} = 'da';
|
||||
}
|
||||
}
|
||||
|
||||
if (exists($args{params}) and ref($args{params}) eq 'HASH') {
|
||||
%params = (%params, %{$args{params}});
|
||||
}
|
||||
|
||||
$url = $self->_append_url_args($url, %params);
|
||||
my $result = $self->_get_initial_data($url);
|
||||
|
||||
# When /c/ failed, try /user/
|
||||
if ((!defined($result) or !scalar(keys %$result)) and $url =~ s{/c/}{/user/}) {
|
||||
$result = $self->_get_initial_data($url);
|
||||
}
|
||||
|
||||
($url, $result);
|
||||
}
|
||||
|
||||
sub _prepare_results_for_return {
|
||||
my ($self, $results, %args) = @_;
|
||||
|
||||
(defined($results) and ref($results) eq 'ARRAY') || return;
|
||||
|
||||
my @results = @$results;
|
||||
|
||||
@results || return;
|
||||
|
||||
if (@results and $results[-1]{type} eq 'nextpage') {
|
||||
|
||||
my $nextpage = pop(@results);
|
||||
|
||||
if (defined($nextpage->{token}) and @results) {
|
||||
|
||||
if ($self->get_debug) {
|
||||
say STDERR ":: Returning results with a continuation page token...";
|
||||
}
|
||||
|
||||
return {
|
||||
url => $args{url},
|
||||
results => {
|
||||
entries => \@results,
|
||||
continuation => $nextpage->{token},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
my $url = $args{url};
|
||||
|
||||
if ($url =~ m{^https://m\.youtube\.com}) {
|
||||
$url = undef;
|
||||
}
|
||||
|
||||
return {
|
||||
url => $url,
|
||||
results => \@results,
|
||||
};
|
||||
}
|
||||
|
||||
=head2 yt_search(q => $keyword, %args)
|
||||
|
||||
Search for videos given a keyword string (uri-escaped).
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_search {
|
||||
my ($self, %args) = @_;
|
||||
|
||||
my $url = $self->get_m_youtube_url . "/results?search_query=$args{q}";
|
||||
|
||||
my @sp;
|
||||
my %params = (hl => 'en',);
|
||||
|
||||
$args{type} //= 'video';
|
||||
|
||||
if ($args{type} eq 'video') {
|
||||
|
||||
if (defined(my $duration = $self->get_videoDuration)) {
|
||||
if ($duration eq 'long') {
|
||||
push @sp, 'EgQQARgC';
|
||||
}
|
||||
elsif ($duration eq 'short') {
|
||||
push @sp, 'EgQQARgB';
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(my $date = $self->get_date)) {
|
||||
if ($date eq 'hour') {
|
||||
push @sp, 'EgQIARAB';
|
||||
}
|
||||
elsif ($date eq 'today') {
|
||||
push @sp, "EgQIAhAB";
|
||||
}
|
||||
elsif ($date eq 'week') {
|
||||
push @sp, "EgQIAxAB";
|
||||
}
|
||||
elsif ($date eq 'month') {
|
||||
push @sp, "EgQIBBAB";
|
||||
}
|
||||
elsif ($date eq 'year') {
|
||||
push @sp, "EgQIBRAB";
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(my $order = $self->get_order)) {
|
||||
if ($order eq 'upload_date') {
|
||||
push @sp, "CAISAhAB";
|
||||
}
|
||||
elsif ($order eq 'view_count') {
|
||||
push @sp, "CAMSAhAB";
|
||||
}
|
||||
elsif ($order eq 'rating') {
|
||||
push @sp, "CAESAhAB";
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(my $license = $self->get_videoLicense)) {
|
||||
if ($license eq 'creative_commons') {
|
||||
push @sp, "EgIwAQ%253D%253D";
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(my $vd = $self->get_videoDefinition)) {
|
||||
if ($vd eq 'high') {
|
||||
push @sp, "EgIgAQ%253D%253D";
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(my $vc = $self->get_videoCaption)) {
|
||||
if ($vc eq 'true' or $vc eq '1') {
|
||||
push @sp, "EgIoAQ%253D%253D";
|
||||
}
|
||||
}
|
||||
|
||||
if (defined(my $vd = $self->get_videoDimension)) {
|
||||
if ($vd eq '3d') {
|
||||
push @sp, "EgI4AQ%253D%253D";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($args{type} eq 'video') {
|
||||
push @sp, "EgIQAQ%253D%253D";
|
||||
}
|
||||
elsif ($args{type} eq 'playlist') {
|
||||
push @sp, "EgIQAw%253D%253D";
|
||||
}
|
||||
elsif ($args{type} eq 'channel') {
|
||||
push @sp, "EgIQAg%253D%253D";
|
||||
}
|
||||
elsif ($args{type} eq 'movie') { # TODO: implement support for movies
|
||||
push @sp, "EgIQBA%253D%253D";
|
||||
}
|
||||
|
||||
$params{sp} = join(',', @sp);
|
||||
$url = $self->_append_url_args($url, %params);
|
||||
|
||||
my $hash = $self->_get_initial_data($url) // return;
|
||||
my @results = $self->_extract_sectionList_results(eval { $hash->{contents}{sectionListRenderer} } // undef, %args);
|
||||
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head2 yt_channel_search($channel, q => $keyword, %args)
|
||||
|
||||
Search for videos given a keyword string (uri-escaped) from a given channel ID or username.
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_channel_search {
|
||||
my ($self, $channel, %args) = @_;
|
||||
my ($url, $hash) = $self->_channel_data($channel, %args, type => 'search', params => {query => $args{q}});
|
||||
|
||||
$hash // return;
|
||||
|
||||
my @results = $self->_extract_sectionList_results($self->_find_sectionList($hash), %args, type => 'video');
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head2 yt_channel_uploads($channel, %args)
|
||||
|
||||
Latest uploads for a given channel ID or username.
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_channel_uploads {
|
||||
my ($self, $channel, %args) = @_;
|
||||
my ($url, $hash) = $self->_channel_data($channel, %args, type => 'videos');
|
||||
|
||||
$hash // return;
|
||||
|
||||
my @results = $self->_extract_channel_uploads($hash, %args, type => 'video');
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head2 yt_channel_playlists($channel, %args)
|
||||
|
||||
Playlists for a given channel ID or username.
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_channel_playlists {
|
||||
my ($self, $channel, %args) = @_;
|
||||
my ($url, $hash) = $self->_channel_data($channel, %args, type => 'playlists');
|
||||
|
||||
$hash // return;
|
||||
|
||||
my @results = $self->_extract_channel_playlists($hash, %args, type => 'playlist');
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head2 yt_playlist_videos($playlist_id, %args)
|
||||
|
||||
Videos from a given playlist ID.
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_playlist_videos {
|
||||
my ($self, $playlist_id, %args) = @_;
|
||||
|
||||
my $url = $self->_append_url_args($self->get_m_youtube_url . "/playlist", list => $playlist_id, hl => "en");
|
||||
my $hash = $self->_get_initial_data($url) // return;
|
||||
|
||||
my @results = $self->_extract_sectionList_results($self->_find_sectionList($hash), %args, type => 'video');
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head2 yt_playlist_next_page($url, $token, %args)
|
||||
|
||||
Load more items from a playlist, given a continuation token.
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_playlist_next_page {
|
||||
my ($self, $url, $token, %args) = @_;
|
||||
|
||||
my $request_url = $self->_append_url_args($url, ctoken => $token);
|
||||
my $hash = $self->_get_initial_data($request_url) // return;
|
||||
|
||||
my @results = $self->_parse_itemSection(
|
||||
eval { $hash->{continuationContents}{playlistVideoListContinuation} }
|
||||
// eval { $hash->{continuationContents}{itemSectionContinuation} },
|
||||
%args
|
||||
);
|
||||
|
||||
if (!@results) {
|
||||
@results =
|
||||
$self->_extract_sectionList_results(eval { $hash->{continuationContents}{sectionListContinuation} } // undef, %args);
|
||||
}
|
||||
|
||||
$self->_add_author_to_results($hash, \@results, %args);
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head2 yt_search_next_page($url, $token, %args)
|
||||
|
||||
Load more search results, given a continuation token.
|
||||
|
||||
=cut
|
||||
|
||||
sub yt_search_next_page {
|
||||
my ($self, $url, $token, %args) = @_;
|
||||
|
||||
my %request = (
|
||||
"context" => {
|
||||
"client" => {
|
||||
"browserName" => "Firefox",
|
||||
"browserVersion" => "82.0",
|
||||
"clientFormFactor" => "LARGE_FORM_FACTOR",
|
||||
"clientName" => "MWEB",
|
||||
"clientVersion" => "2.20201030.01.00",
|
||||
"deviceMake" => "generic",
|
||||
"deviceModel" => "android 11.0",
|
||||
"gl" => "US",
|
||||
"hl" => "en",
|
||||
"mainAppWebInfo" => {
|
||||
"graftUrl" => "https://m.youtube.com/results?search_query=youtube"
|
||||
},
|
||||
"osName" => "Android",
|
||||
"osVersion" => "10",
|
||||
"platform" => "TABLET",
|
||||
"playerType" => "UNIPLAYER",
|
||||
"screenDensityFloat" => 1,
|
||||
"screenHeightPoints" => 420,
|
||||
"screenPixelDensity" => 1,
|
||||
"screenWidthPoints" => 1442,
|
||||
"userAgent" => "Mozilla/5.0 (Android 10; Tablet; rv:82.0) Gecko/82.0 Firefox/82.0,gzip(gfe)",
|
||||
"userInterfaceTheme" => "USER_INTERFACE_THEME_LIGHT",
|
||||
"utcOffsetMinutes" => 0,
|
||||
},
|
||||
"request" => {
|
||||
"consistencyTokenJars" => [],
|
||||
"internalExperimentFlags" => [],
|
||||
},
|
||||
"user" => {}
|
||||
},
|
||||
"continuation" => $token,
|
||||
);
|
||||
|
||||
my $content = $self->post_as_json(
|
||||
$self->get_m_youtube_url
|
||||
. _unscramble('o/ebseky?u1ri//hvcuyta=e')
|
||||
. _unscramble('1HUCiSlOalFEcYQSS8_9q1LW4y8JAwI2zT_qA_G'),
|
||||
\%request
|
||||
) // return;
|
||||
|
||||
my $hash = $self->parse_json_string($content);
|
||||
|
||||
my @results = $self->_extract_sectionList_results(
|
||||
scalar {
|
||||
contents => eval {
|
||||
$hash->{onResponseReceivedCommands}[0]{appendContinuationItemsAction}{continuationItems};
|
||||
} // undef
|
||||
},
|
||||
%args
|
||||
);
|
||||
|
||||
$self->_prepare_results_for_return(\@results, %args, url => $url);
|
||||
}
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >>
|
||||
|
||||
|
||||
=head1 SUPPORT
|
||||
|
||||
You can find documentation for this module with the perldoc command.
|
||||
|
||||
perldoc WWW::PipeViewer::InitialData
|
||||
|
||||
|
||||
=head1 LICENSE AND COPYRIGHT
|
||||
|
||||
Copyright 2013-2015 Trizen.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it
|
||||
under the terms of either: the GNU General Public License as published
|
||||
by the Free Software Foundation; or the Artistic License.
|
||||
|
||||
See L<http://dev.perl.org/licenses/> for more information.
|
||||
|
||||
=cut
|
||||
|
||||
1; # End of WWW::PipeViewer::InitialData
|
||||
@@ -41,90 +41,90 @@ Reference: http://en.wikipedia.org/wiki/YouTube#Quality_and_formats
|
||||
sub get_itags {
|
||||
scalar {
|
||||
|
||||
'best' => [{value => 38, format => 'mp4'}, # mp4 (3072p) (v-a)
|
||||
{value => 138, format => 'mp4', dash => 1}, # mp4 (2160p-4320p) (v)
|
||||
{value => 266, format => 'mp4', dash => 1}, # mp4 (2160p-2304p) (v)
|
||||
'best' => [{value => 38, format => 'mp4'}, # mp4 (3072p) (v-a)
|
||||
{value => 138, format => 'mp4', split => 1}, # mp4 (2160p-4320p) (v)
|
||||
{value => 266, format => 'mp4', split => 1}, # mp4 (2160p-2304p) (v)
|
||||
],
|
||||
|
||||
'2160' => [{value => 315, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 272, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 313, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 401, format => 'av1', dash => 1}, # av1 (v)
|
||||
'2160' => [{value => 315, format => 'webm', split => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 272, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 313, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 401, format => 'av1', split => 1}, # av1 (v)
|
||||
],
|
||||
|
||||
'1440' => [{value => 308, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 271, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 264, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 400, format => 'av1', dash => 1}, # av1 (v)
|
||||
'1440' => [{value => 308, format => 'webm', split => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 271, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 264, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 400, format => 'av1', split => 1}, # av1 (v)
|
||||
],
|
||||
|
||||
'1080' => [{value => 303, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 299, format => 'mp4', dash => 1, hfr => 1}, # mp4 HFR (v)
|
||||
{value => 248, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 137, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 399, format => 'av1', dash => 1, hfr => 1}, # av1 (v)
|
||||
{value => 46, format => 'webm'}, # webm (v-a)
|
||||
{value => 37, format => 'mp4'}, # mp4 (v-a)
|
||||
{value => 301, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 96, format => 'ts', live => 1}, # ts (live) (v-a)
|
||||
'1080' => [{value => 303, format => 'webm', split => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 299, format => 'mp4', split => 1, hfr => 1}, # mp4 HFR (v)
|
||||
{value => 248, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 137, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 399, format => 'av1', split => 1, hfr => 1}, # av1 (v)
|
||||
{value => 46, format => 'webm'}, # webm (v-a)
|
||||
{value => 37, format => 'mp4'}, # mp4 (v-a)
|
||||
{value => 301, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 96, format => 'ts', live => 1}, # ts (live) (v-a)
|
||||
],
|
||||
|
||||
'720' => [{value => 302, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 298, format => 'mp4', dash => 1, hfr => 1}, # mp4 HFR (v)
|
||||
{value => 247, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 136, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 398, format => 'av1', dash => 1, hfr => 1}, # av1 (v)
|
||||
{value => 45, format => 'webm'}, # webm (v-a)
|
||||
{value => 22, format => 'mp4'}, # mp4 (v-a)
|
||||
{value => 300, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 120, format => 'flv', live => 1}, # flv (live) (v-a)
|
||||
{value => 95, format => 'ts', live => 1}, # ts (live) (v-a)
|
||||
'720' => [{value => 302, format => 'webm', split => 1, hfr => 1}, # webm HFR (v)
|
||||
{value => 298, format => 'mp4', split => 1, hfr => 1}, # mp4 HFR (v)
|
||||
{value => 247, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 136, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 398, format => 'av1', split => 1, hfr => 1}, # av1 (v)
|
||||
{value => 45, format => 'webm'}, # webm (v-a)
|
||||
{value => 22, format => 'mp4'}, # mp4 (v-a)
|
||||
{value => 300, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 120, format => 'flv', live => 1}, # flv (live) (v-a)
|
||||
{value => 95, format => 'ts', live => 1}, # ts (live) (v-a)
|
||||
],
|
||||
|
||||
'480' => [{value => 244, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 135, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 397, format => 'av1', dash => 1}, # av1 (v)
|
||||
{value => 44, format => 'webm'}, # webm (v-a)
|
||||
{value => 35, format => 'flv'}, # flv (v-a)
|
||||
{value => 94, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
'480' => [{value => 244, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 135, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 397, format => 'av1', split => 1}, # av1 (v)
|
||||
{value => 44, format => 'webm'}, # webm (v-a)
|
||||
{value => 35, format => 'flv'}, # flv (v-a)
|
||||
{value => 94, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
],
|
||||
|
||||
'360' => [{value => 243, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 134, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 396, format => 'av1', dash => 1}, # av1 (v)
|
||||
{value => 43, format => 'webm'}, # webm (v-a)
|
||||
{value => 34, format => 'flv'}, # flv (v-a)
|
||||
{value => 93, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 18, format => 'mp4'}, # mp4 (v-a)
|
||||
'360' => [{value => 243, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 134, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 396, format => 'av1', split => 1}, # av1 (v)
|
||||
{value => 43, format => 'webm'}, # webm (v-a)
|
||||
{value => 34, format => 'flv'}, # flv (v-a)
|
||||
{value => 93, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 18, format => 'mp4'}, # mp4 (v-a)
|
||||
],
|
||||
|
||||
'240' => [{value => 242, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 133, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 395, format => 'av1', dash => 1}, # av1 (v)
|
||||
{value => 6, format => 'flv'}, # flv (270p) (v-a)
|
||||
{value => 5, format => 'flv'}, # flv (v-a)
|
||||
{value => 36, format => '3gp'}, # 3gp (v-a)
|
||||
{value => 13, format => '3gp'}, # 3gp (v-a)
|
||||
{value => 92, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 132, format => 'ts', live => 1}, # ts (live) (v-a)
|
||||
'240' => [{value => 242, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 133, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 395, format => 'av1', split => 1}, # av1 (v)
|
||||
{value => 6, format => 'flv'}, # flv (270p) (v-a)
|
||||
{value => 5, format => 'flv'}, # flv (v-a)
|
||||
{value => 36, format => '3gp'}, # 3gp (v-a)
|
||||
{value => 13, format => '3gp'}, # 3gp (v-a)
|
||||
{value => 92, format => 'mp4', live => 1}, # mp4 (live) (v-a)
|
||||
{value => 132, format => 'ts', live => 1}, # ts (live) (v-a)
|
||||
],
|
||||
|
||||
'144' => [{value => 278, format => 'webm', dash => 1}, # webm (v)
|
||||
{value => 160, format => 'mp4', dash => 1}, # mp4 (v)
|
||||
{value => 394, format => 'av1', dash => 1}, # av1 (v)
|
||||
{value => 17, format => '3gp'}, # 3gp (v-a)
|
||||
{value => 91, format => 'mp4'}, # mp4 (live) (v-a)
|
||||
{value => 151, format => 'ts'}, # ts (live) (v-a)
|
||||
'144' => [{value => 278, format => 'webm', split => 1}, # webm (v)
|
||||
{value => 160, format => 'mp4', split => 1}, # mp4 (v)
|
||||
{value => 394, format => 'av1', split => 1}, # av1 (v)
|
||||
{value => 17, format => '3gp'}, # 3gp (v-a)
|
||||
{value => 91, format => 'mp4'}, # mp4 (live) (v-a)
|
||||
{value => 151, format => 'ts'}, # ts (live) (v-a)
|
||||
],
|
||||
|
||||
'audio' => [{value => 172, format => 'webm', kbps => 192}, # webm (192 kbps)
|
||||
{value => 251, format => 'opus', kbps => 160}, # webm opus (128-160 kbps)
|
||||
{value => 171, format => 'webm', kbps => 128}, # webm vorbis (92-128 kbps)
|
||||
{value => 140, format => 'm4a', kbps => 128}, # mp4a (128 kbps)
|
||||
{value => 141, format => 'm4a', kbps => 256}, # mp4a (256 kbps)
|
||||
{value => 250, format => 'opus', kbps => 64}, # webm opus (64 kbps)
|
||||
{value => 249, format => 'opus', kbps => 48}, # webm opus (48 kbps)
|
||||
{value => 139, format => 'm4a', kbps => 48}, # mp4a (48 kbps)
|
||||
'audio' => [{value => 172, format => 'webm', kbps => 192}, # webm (192 kbps)
|
||||
{value => 251, format => 'opus', kbps => 160}, # webm opus (128-160 kbps)
|
||||
{value => 171, format => 'webm', kbps => 128}, # webm vorbis (92-128 kbps)
|
||||
{value => 140, format => 'm4a', kbps => 128}, # mp4a (128 kbps)
|
||||
{value => 141, format => 'm4a', kbps => 256}, # mp4a (256 kbps)
|
||||
{value => 250, format => 'opus', kbps => 64}, # webm opus (64 kbps)
|
||||
{value => 249, format => 'opus', kbps => 48}, # webm opus (48 kbps)
|
||||
{value => 139, format => 'm4a', kbps => 48}, # mp4a (48 kbps)
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -176,12 +176,12 @@ sub _find_streaming_url {
|
||||
$args{ignore_av1} && next; # ignore videos in AV1 format
|
||||
}
|
||||
|
||||
if ($itag->{dash}) {
|
||||
if ($itag->{split}) {
|
||||
|
||||
$args{dash} || next;
|
||||
$args{split} || next;
|
||||
|
||||
my $video_info = $stream->{$itag->{value}};
|
||||
my $audio_info = $self->_find_streaming_url(%args, resolution => 'audio', dash => 0);
|
||||
my $audio_info = $self->_find_streaming_url(%args, resolution => 'audio', split => 0);
|
||||
|
||||
if (defined($audio_info)) {
|
||||
$video_info->{__AUDIO__} = $audio_info;
|
||||
@@ -191,14 +191,14 @@ sub _find_streaming_url {
|
||||
next;
|
||||
}
|
||||
|
||||
if ($resolution eq 'audio' and not $args{dash_mp4_audio}) {
|
||||
if ($resolution eq 'audio' and not $args{m4a_audio}) {
|
||||
if ($itag->{format} eq 'm4a') {
|
||||
next; # skip m4a audio URLs
|
||||
}
|
||||
}
|
||||
|
||||
# Ignore segmented DASH URLs (they load pretty slow in mpv)
|
||||
if (not $args{dash_segmented}) {
|
||||
if (not $args{dash}) {
|
||||
next if ($entry->{url} =~ m{/api/manifest/dash/});
|
||||
}
|
||||
|
||||
@@ -215,9 +215,12 @@ Return the streaming URL which corresponds with the specified resolution.
|
||||
(
|
||||
urls => \@streaming_urls,
|
||||
resolution => 'resolution_name', # from $obj->get_resolutions(),
|
||||
dash => 1/0, # include or exclude DASH itags
|
||||
dash_mp4_audio => 1/0, # include or exclude DASH videos with MP4 audio
|
||||
dash_segmented => 1/0, # include or exclude segmented DASH videos
|
||||
|
||||
hfr => 1/0, # include or exclude High Frame Rate videos
|
||||
ignore_av1 => 1/0, # true to ignore videos in AV1 format
|
||||
split => 1/0, # include or exclude split videos
|
||||
m4a_audio => 1/0, # incldue or exclude M4A audio files
|
||||
dash => 1/0, # include or exclude streams in DASH format
|
||||
)
|
||||
|
||||
=cut
|
||||
|
||||
@@ -31,7 +31,7 @@ sub parse_utf8_json_string {
|
||||
}
|
||||
|
||||
require JSON;
|
||||
my $hash = eval { JSON::from_json($json) };
|
||||
my $hash = eval { JSON::from_json($json) };
|
||||
return $@ ? do { warn "[JSON]: $@\n"; {} } : $hash;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ sub parse_json_string {
|
||||
}
|
||||
|
||||
require JSON;
|
||||
my $hash = eval { JSON::decode_json($json) };
|
||||
my $hash = eval { JSON::decode_json($json) };
|
||||
return $@ ? do { warn "[JSON]: $@\n"; {} } : $hash;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,13 @@ Get videos from a specific playlistID.
|
||||
|
||||
sub videos_from_playlist_id {
|
||||
my ($self, $id) = @_;
|
||||
$self->_get_results($self->_make_feed_url("playlists/$id"));
|
||||
|
||||
if (my $results = $self->yt_playlist_videos($id)) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
my $url = $self->_make_feed_url("playlists/$id");
|
||||
$self->_get_results($url);
|
||||
}
|
||||
|
||||
=head2 favorites($channel_id)
|
||||
|
||||
@@ -25,10 +25,7 @@ sub _make_playlists_url {
|
||||
$opts{'part'} = 'snippet,contentDetails';
|
||||
}
|
||||
|
||||
$self->_make_feed_url(
|
||||
'playlists',
|
||||
%opts,
|
||||
);
|
||||
$self->_make_feed_url('playlists', %opts,);
|
||||
}
|
||||
|
||||
sub get_playlist_id {
|
||||
@@ -63,7 +60,13 @@ Get and return playlists from a channel ID.
|
||||
|
||||
sub playlists {
|
||||
my ($self, $channel_id) = @_;
|
||||
$self->_get_results($self->_make_feed_url("channels/playlists/$channel_id"));
|
||||
|
||||
if (my $results = $self->yt_channel_playlists($channel_id)) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
my $url = $self->_make_feed_url("channels/playlists/$channel_id");
|
||||
$self->_get_results($url);
|
||||
}
|
||||
|
||||
=head2 playlists_from_username($username)
|
||||
|
||||
@@ -27,9 +27,9 @@ our $non_digit_or_opt_re = qr{^(?!$range_num_re)(?>[0-9]{1,3}[^0-9]|[0-9]{4}|[^0
|
||||
|
||||
# Generic name
|
||||
my $generic_name_re = qr/[a-zA-Z0-9_.\-]{11,64}/;
|
||||
our $valid_channel_id_re = qr{^(?:.*/channel/)?(?<channel_id>(?:\w+(?:[-.]++\w++)*|$generic_name_re))(?:/.*)?\z};
|
||||
our $valid_channel_id_re = qr{^(?:.*/(?:channel|c)/)?(?<channel_id>(?:[%\w]+(?:[-.]++[%\w]++)*|$generic_name_re))(?:/.*)?\z};
|
||||
|
||||
our $get_channel_videos_id_re = qr{^.*/channel/(?<channel_id>(?:\w+(?:[-.]++\w++)*|$generic_name_re))};
|
||||
our $get_channel_videos_id_re = qr{^.*/(?:channel|c)/(?<channel_id>(?:[%\w]+(?:[-.]++[%\w]++)*|$generic_name_re))};
|
||||
our $get_channel_playlists_id_re = qr{$get_channel_videos_id_re/playlists};
|
||||
|
||||
our $get_username_videos_re = qr{^.*/user/(?<username>[-.\w]+)};
|
||||
|
||||
+14
-210
@@ -18,207 +18,6 @@ WWW::PipeViewer::Search - Search for stuff on YouTube
|
||||
|
||||
=cut
|
||||
|
||||
sub _time_to_seconds {
|
||||
my ($time) = @_;
|
||||
|
||||
my ($hours, $minutes, $seconds) = (0, 0, 0);
|
||||
|
||||
if ($time =~ /(\d+):(\d+):(\d+)/) {
|
||||
($hours, $minutes, $seconds) = ($1, $2, $3);
|
||||
}
|
||||
elsif ($time =~ /(\d+):(\d+)/) {
|
||||
($minutes, $seconds) = ($1, $2);
|
||||
}
|
||||
elsif ($time =~ /(\d+)/) {
|
||||
$seconds = $1;
|
||||
}
|
||||
|
||||
$hours * 3600 + $minutes * 60 + $seconds;
|
||||
}
|
||||
|
||||
sub _view_count_text_to_int {
|
||||
my ($text) = @_;
|
||||
|
||||
if ($text =~ /([\d,.]+)/) {
|
||||
my $v = $1;
|
||||
$v =~ tr/.,//d;
|
||||
return $v;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _thumbnail_quality {
|
||||
my ($width, $height) = @_;
|
||||
|
||||
$width // return 'medium';
|
||||
$height // return 'medium';
|
||||
|
||||
if ($width == 1280 and $height == 720) {
|
||||
return "maxres";
|
||||
}
|
||||
|
||||
if ($width == 640 and $height == 480) {
|
||||
return "sddefault";
|
||||
}
|
||||
|
||||
if ($width == 480 and $height == 360) {
|
||||
return 'high';
|
||||
}
|
||||
|
||||
if ($width == 320 and $height == 180) {
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
if ($width == 120 and $height == 90) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
sub _extract_youtube_mix {
|
||||
my ($self, $data) = @_;
|
||||
|
||||
my $info = eval { $data->{callToAction}{watchCardHeroVideoRenderer} } || return;
|
||||
my $header = eval { $data->{header}{watchCardRichHeaderRenderer} };
|
||||
|
||||
my %mix;
|
||||
|
||||
$mix{type} = 'playlist';
|
||||
|
||||
$mix{title} =
|
||||
eval { $header->{title}{runs}[0]{text} }
|
||||
// eval { $info->{accessibility}{accessibilityData}{label} }
|
||||
// eval { $info->{callToActionButton}{callToActionButtonRenderer}{label}{runs}[0]{text} } // 'Youtube Mix';
|
||||
|
||||
$mix{playlistId} = eval { $info->{navigationEndpoint}{watchEndpoint}{playlistId} } || return;
|
||||
|
||||
$mix{playlistThumbnail} = eval { $header->{avatar}{thumbnails}[0]{url} }
|
||||
// eval { $info->{heroImage}{collageHeroImageRenderer}{leftThumbnail}{thumbnails}[0]{url} };
|
||||
|
||||
$mix{author} = eval { $header->{title}{runs}[0]{text} } // "YouTube";
|
||||
$mix{authorId} = eval { $header->{titleNavigationEndpoint}{browseEndpoint}{browseId} } // "youtube";
|
||||
|
||||
return \%mix;
|
||||
}
|
||||
|
||||
sub _extract_search_entry {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
# Album
|
||||
if ($args{type} eq 'all' and exists $data->{horizontalCardListRenderer}) { # TODO
|
||||
return;
|
||||
}
|
||||
|
||||
# Video
|
||||
if (exists $data->{compactVideoRenderer}) {
|
||||
|
||||
my %video;
|
||||
|
||||
my $info = $data->{compactVideoRenderer};
|
||||
|
||||
$video{title} =
|
||||
eval { $info->{title}{runs}[0]{text} } // eval { $info->{title}{accessibility}{accessibilityData}{label} } // return;
|
||||
$video{videoId} = eval { $info->{navigationEndpoint}{watchEndpoint}{videoId} } // $info->{videoId} // return;
|
||||
$video{author} = eval { $info->{longBylineText}{runs}[0]{text} } // eval { $info->{shortBylineText}{runs}[0]{text} };
|
||||
$video{authorId} = $info->{channelId};
|
||||
$video{publishedText} = eval { $info->{publishedTimeText}{runs}[0]{text} };
|
||||
$video{viewCountText} = eval { $info->{shortViewCountText}{runs}[0]{text} };
|
||||
$video{videoThumbnails} = eval {
|
||||
[
|
||||
map {
|
||||
my %thumb = %$_;
|
||||
$thumb{quality} = _thumbnail_quality($thumb{width}, $thumb{height});
|
||||
\%thumb;
|
||||
} @{$info->{thumbnail}{thumbnails}}
|
||||
]
|
||||
};
|
||||
|
||||
# FIXME: this is not the video description
|
||||
$video{description} = eval { $info->{title}{accessibility}{accessibilityData}{label} };
|
||||
|
||||
my $time = eval { $info->{thumbnailOverlays}[0]{thumbnailOverlayTimeStatusRenderer}{text}{runs}[0]{text} };
|
||||
|
||||
if (defined($time)) {
|
||||
$video{lengthSeconds} = eval { _time_to_seconds($time) };
|
||||
}
|
||||
|
||||
$video{title} = eval { $info->{title}{runs}[0]{text} };
|
||||
|
||||
my $viewCountText = eval { $info->{viewCountText}{runs}[0]{text} };
|
||||
|
||||
if (defined($viewCountText)) {
|
||||
$video{viewCount} = _view_count_text_to_int($viewCountText);
|
||||
}
|
||||
|
||||
return \%video;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub _extract_search_results {
|
||||
my ($self, $data, %args) = @_;
|
||||
|
||||
eval { ref($data->{contents}{sectionListRenderer}{contents}) eq 'ARRAY' } or return;
|
||||
|
||||
my @results;
|
||||
|
||||
foreach my $entry (@{$data->{contents}{sectionListRenderer}{contents}}) {
|
||||
|
||||
# YouTube Mix
|
||||
if ($args{type} eq 'all' and exists $entry->{universalWatchCardRenderer}) {
|
||||
|
||||
my $mix = $self->_extract_youtube_mix($entry->{universalWatchCardRenderer});
|
||||
|
||||
if (defined($mix)) {
|
||||
push(@results, $mix);
|
||||
}
|
||||
}
|
||||
|
||||
# Search results
|
||||
if (exists $entry->{itemSectionRenderer}) {
|
||||
|
||||
eval { ref($entry->{itemSectionRenderer}{contents}) eq 'ARRAY' } || next;
|
||||
|
||||
foreach my $entry (@{$entry->{itemSectionRenderer}{contents}}) {
|
||||
|
||||
my $search_entry = $self->_extract_search_entry($entry, %args);
|
||||
|
||||
if (defined($search_entry)) {
|
||||
|
||||
#use Data::Dump qw(pp);
|
||||
#pp $search_entry;
|
||||
push @results, $search_entry;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
# Continuation page
|
||||
if (exists $entry->{continuationItemRenderer}) { # TODO
|
||||
## ...
|
||||
}
|
||||
}
|
||||
|
||||
return @results;
|
||||
}
|
||||
|
||||
sub _youtube_search {
|
||||
my ($self, %args) = @_;
|
||||
|
||||
my $content = $self->lwp_get($self->get_m_youtube_url . "/results?search_query=$args{q}");
|
||||
|
||||
if ($content =~ m{<div id="initial-data"><!--(.*?)--></div>}is) {
|
||||
my $json = $1;
|
||||
my $hash = $self->parse_utf8_json_string($json);
|
||||
return $self->_extract_search_results($hash, %args);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub _make_search_url {
|
||||
my ($self, %opts) = @_;
|
||||
|
||||
@@ -286,22 +85,26 @@ sub search_for {
|
||||
|
||||
# Search in a channel's videos
|
||||
if (defined(my $channel_id = $self->get_channelId)) {
|
||||
my $url = $self->_make_feed_url("channels/search/$channel_id", q => $keywords,);
|
||||
|
||||
$self->set_channelId(); # clear the channel ID
|
||||
|
||||
if (my $results = $self->yt_channel_search($channel_id, q => $keywords, type => $type, %$args)) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
my $url = $self->_make_feed_url("channels/search/$channel_id", q => $keywords);
|
||||
return $self->_get_results($url);
|
||||
}
|
||||
|
||||
if (my $results = $self->yt_search(q => $keywords, type => $type, %$args)) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
my $url = $self->_make_search_url(
|
||||
type => $type,
|
||||
q => $keywords,
|
||||
%$args,
|
||||
%$args
|
||||
);
|
||||
|
||||
return
|
||||
scalar {
|
||||
url => $url,
|
||||
results => [$self->_youtube_search(q => $keywords, type => $type, %$args)],
|
||||
};
|
||||
|
||||
return $self->_get_results($url);
|
||||
}
|
||||
|
||||
@@ -370,6 +173,7 @@ sub related_to_videoID {
|
||||
|
||||
my %info = $self->_get_video_info($videoID);
|
||||
my $watch_next_response = $self->parse_json_string($info{watch_next_response});
|
||||
|
||||
my $related =
|
||||
eval { $watch_next_response->{contents}{twoColumnWatchNextResults}{secondaryResults}{secondaryResults}{results} }
|
||||
// return {results => []};
|
||||
|
||||
@@ -223,31 +223,33 @@ Returns true if a given result has entries.
|
||||
sub has_entries {
|
||||
my ($self, $result) = @_;
|
||||
|
||||
$result // return 0;
|
||||
|
||||
if (ref($result->{results}) eq 'HASH') {
|
||||
|
||||
foreach my $type(qw(comments videos playlists)) {
|
||||
foreach my $type (qw(comments videos playlists entries)) {
|
||||
if (exists $result->{results}{$type}) {
|
||||
return scalar @{$result->{results}{$type}} > 0;
|
||||
ref($result->{results}{$type}) eq 'ARRAY' or return 0;
|
||||
return (@{$result->{results}{$type}} > 0);
|
||||
}
|
||||
}
|
||||
|
||||
my $type = $result->{results}{type} // '';
|
||||
|
||||
if ($type eq 'playlist') {
|
||||
return $result->{results}{videoCount} > 0;
|
||||
return ($result->{results}{videoCount} > 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (ref($result->{results}) eq 'ARRAY') {
|
||||
return scalar(@{$result->{results}}) > 0;
|
||||
return (@{$result->{results}} > 0);
|
||||
}
|
||||
|
||||
if (ref($result->{results}) eq 'HASH' and not keys %{$result->{results}}) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1; # maybe?
|
||||
#ref($result) eq 'HASH' and ($result->{results}{pageInfo}{totalResults} > 0);
|
||||
return 1; # maybe?
|
||||
}
|
||||
|
||||
=head2 normalize_video_title($title, $fat32safe)
|
||||
@@ -384,8 +386,8 @@ sub format_text {
|
||||
$text =~ s/$escapes_re/$special_escapes{$1}/g;
|
||||
|
||||
$escape
|
||||
? $text =~ s/$tokens_re/\Q${\$special_tokens{$1}()}\E/gr
|
||||
: $text =~ s/$tokens_re/${\$special_tokens{$1}()}/gr;
|
||||
? $text =~ s<$tokens_re><\Q${\($special_tokens{$1}() // '')}\E>gr
|
||||
: $text =~ s<$tokens_re><${\($special_tokens{$1}() // '')}>gr;
|
||||
}
|
||||
|
||||
=head2 set_thousands($num)
|
||||
@@ -421,22 +423,6 @@ Get videoID.
|
||||
sub get_video_id {
|
||||
my ($self, $info) = @_;
|
||||
$info->{videoId};
|
||||
|
||||
#~ ref($info->{id}) eq 'HASH' ? $info->{id}{videoId}
|
||||
#~ : exists($info->{snippet}{resourceId}{videoId}) ? $info->{snippet}{resourceId}{videoId}
|
||||
#~ : exists($info->{contentDetails}{videoId}) ? $info->{contentDetails}{videoId}
|
||||
#~ : exists($info->{contentDetails}{playlistItem}{resourceId}{videoId})
|
||||
#~ ? $info->{contentDetails}{playlistItem}{resourceId}{videoId}
|
||||
#~ : exists($info->{contentDetails}{upload}{videoId}) ? $info->{contentDetails}{upload}{videoId}
|
||||
#~ : do {
|
||||
#~ my $id = $info->{id} // return undef;
|
||||
|
||||
#~ if (length($id) != 11) {
|
||||
#~ return undef;
|
||||
#~ }
|
||||
|
||||
#~ $id;
|
||||
#~ };
|
||||
}
|
||||
|
||||
sub get_playlist_id {
|
||||
@@ -538,22 +524,29 @@ sub get_thumbnail_url {
|
||||
|
||||
if ($info->{type} eq 'channel') {
|
||||
ref($info->{authorThumbnails}) eq 'ARRAY' or return '';
|
||||
return $info->{authorThumbnails}[0]{url};
|
||||
|
||||
foreach my $thumbnail (map { ref($_) eq 'ARRAY' ? @{$_} : $_ } @{$info->{authorThumbnails}}) {
|
||||
if (exists $thumbnail->{quality} and $thumbnail->{quality} eq $type) {
|
||||
return $thumbnail->{url};
|
||||
}
|
||||
}
|
||||
|
||||
return eval { $info->{authorThumbnails}[0]{url} } // '';
|
||||
}
|
||||
|
||||
ref($info->{videoThumbnails}) eq 'ARRAY' or return '';
|
||||
|
||||
my @thumbs = @{$info->{videoThumbnails}};
|
||||
my @wanted = grep{$_->{quality} eq $type} @thumbs;
|
||||
my @thumbs = map { ref($_) eq 'ARRAY' ? @{$_} : $_ } @{$info->{videoThumbnails}};
|
||||
my @wanted = grep { $_->{quality} eq $type } grep { ref($_) eq 'HASH' } @thumbs;
|
||||
|
||||
my $url;
|
||||
|
||||
if (@wanted) {
|
||||
$url = $wanted[0]{url};
|
||||
$url = eval { $wanted[0]{url} } // return '';
|
||||
}
|
||||
else {
|
||||
warn "[!] Couldn't find thumbnail of type <<$type>>...";
|
||||
$url = $thumbs[0]{url};
|
||||
$url = eval { $thumbs[0]{url} } // return '';
|
||||
}
|
||||
|
||||
# Clean URL of trackers and other junk
|
||||
@@ -564,8 +557,9 @@ sub get_thumbnail_url {
|
||||
|
||||
sub get_channel_title {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#$info->{snippet}{channelTitle} || $self->get_channel_id($info);
|
||||
$info->{author};
|
||||
$info->{author} // $info->{title};
|
||||
}
|
||||
|
||||
sub get_author {
|
||||
@@ -578,6 +572,16 @@ sub get_comment_id {
|
||||
$info->{commentId};
|
||||
}
|
||||
|
||||
sub get_video_count {
|
||||
my ($self, $info) = @_;
|
||||
$info->{videoCount} // 0;
|
||||
}
|
||||
|
||||
sub get_subscriber_count {
|
||||
my ($self, $info) = @_;
|
||||
$info->{subCount} // 0;
|
||||
}
|
||||
|
||||
sub get_comment_content {
|
||||
my ($self, $info) = @_;
|
||||
$info->{content};
|
||||
@@ -585,18 +589,21 @@ sub get_comment_content {
|
||||
|
||||
sub get_id {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#$info->{id};
|
||||
$info->{videoId};
|
||||
}
|
||||
|
||||
sub get_channel_id {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#$info->{snippet}{resourceId}{channelId} // $info->{snippet}{channelId};
|
||||
$info->{authorId};
|
||||
}
|
||||
|
||||
sub get_category_id {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#$info->{snippet}{resourceId}{categoryId} // $info->{snippet}{categoryId};
|
||||
#"unknown";
|
||||
$info->{genre} // 'Unknown';
|
||||
@@ -630,16 +637,21 @@ sub get_category_name {
|
||||
|
||||
sub get_publication_date {
|
||||
my ($self, $info) = @_;
|
||||
#$self->format_date($info->{snippet}{publishedAt});
|
||||
#$self->format_date
|
||||
|
||||
if (defined $info->{publishedText}) {
|
||||
return $info->{publishedText};
|
||||
}
|
||||
|
||||
require Encode;
|
||||
require Time::Piece;
|
||||
|
||||
my $time = Time::Piece->new($info->{published});
|
||||
$time->strftime("%d %B %Y");
|
||||
Encode::decode_utf8($time->strftime("%d %B %Y"));
|
||||
}
|
||||
|
||||
sub get_publication_age {
|
||||
my ($self, $info) = @_;
|
||||
($info->{publishedText} // '') =~ s/\sago\z//r;;
|
||||
($info->{publishedText} // '') =~ s/\sago\z//r;
|
||||
}
|
||||
|
||||
sub get_publication_age_approx {
|
||||
@@ -672,8 +684,6 @@ sub get_publication_age_approx {
|
||||
|
||||
sub get_duration {
|
||||
my ($self, $info) = @_;
|
||||
#$self->format_duration($info->{contentDetails}{duration});
|
||||
#$self->format_duration($info->{lengthSeconds});
|
||||
$info->{lengthSeconds};
|
||||
}
|
||||
|
||||
@@ -685,12 +695,11 @@ sub get_time {
|
||||
}
|
||||
|
||||
$self->format_time($self->get_duration($info));
|
||||
|
||||
#$self->format_time($self->get_duration($info));
|
||||
}
|
||||
|
||||
sub get_definition {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#uc($info->{contentDetails}{definition} // '-');
|
||||
#...;
|
||||
"unknown";
|
||||
@@ -698,6 +707,7 @@ sub get_definition {
|
||||
|
||||
sub get_dimension {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#uc($info->{contentDetails}{dimension});
|
||||
#...;
|
||||
"unknown";
|
||||
@@ -705,6 +715,7 @@ sub get_dimension {
|
||||
|
||||
sub get_caption {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#$info->{contentDetails}{caption};
|
||||
#...;
|
||||
"unknown";
|
||||
@@ -715,39 +726,44 @@ sub get_views {
|
||||
$info->{viewCount} // 0;
|
||||
}
|
||||
|
||||
sub short_human_number {
|
||||
my ($self, $int) = @_;
|
||||
|
||||
if ($int < 1000) {
|
||||
return $int;
|
||||
}
|
||||
|
||||
if ($int >= 10 * 1e9) { # ten billions
|
||||
return sprintf("%dB", int($int / 1e9));
|
||||
}
|
||||
|
||||
if ($int >= 1e9) { # billions
|
||||
return sprintf("%.2gB", $int / 1e9);
|
||||
}
|
||||
|
||||
if ($int >= 10 * 1e6) { # ten millions
|
||||
return sprintf("%dM", int($int / 1e6));
|
||||
}
|
||||
|
||||
if ($int >= 1e6) { # millions
|
||||
return sprintf("%.2gM", $int / 1e6);
|
||||
}
|
||||
|
||||
if ($int >= 10 * 1e3) { # ten thousands
|
||||
return sprintf("%dK", int($int / 1e3));
|
||||
}
|
||||
|
||||
if ($int >= 1e3) { # thousands
|
||||
return sprintf("%.2gK", $int / 1e3);
|
||||
}
|
||||
|
||||
return $int;
|
||||
}
|
||||
|
||||
sub get_views_approx {
|
||||
my ($self, $info) = @_;
|
||||
my $views = $self->get_views($info);
|
||||
|
||||
if ($views < 1000) {
|
||||
return $views;
|
||||
}
|
||||
|
||||
if ($views >= 10 * 1e9) { # ten billions
|
||||
return sprintf("%dB", int($views / 1e9));
|
||||
}
|
||||
|
||||
if ($views >= 1e9) { # billions
|
||||
return sprintf("%.2gB", $views / 1e9);
|
||||
}
|
||||
|
||||
if ($views >= 10 * 1e6) { # ten millions
|
||||
return sprintf("%dM", int($views / 1e6));
|
||||
}
|
||||
|
||||
if ($views >= 1e6) { # millions
|
||||
return sprintf("%.2gM", $views / 1e6);
|
||||
}
|
||||
|
||||
if ($views >= 10 * 1e3) { # ten thousands
|
||||
return sprintf("%dK", int($views / 1e3));
|
||||
}
|
||||
|
||||
if ($views >= 1e3) { # thousands
|
||||
return sprintf("%.2gK", $views / 1e3);
|
||||
}
|
||||
|
||||
return $views;
|
||||
$self->short_human_number($views);
|
||||
}
|
||||
|
||||
sub get_likes {
|
||||
@@ -762,14 +778,14 @@ sub get_dislikes {
|
||||
|
||||
sub get_comments {
|
||||
my ($self, $info) = @_;
|
||||
|
||||
#$info->{statistics}{commentCount};
|
||||
1;
|
||||
}
|
||||
|
||||
{
|
||||
no strict 'refs';
|
||||
foreach my $pair (
|
||||
[playlist => {'playlist' => 1}],
|
||||
foreach my $pair ([playlist => {'playlist' => 1}],
|
||||
[channel => {'channel' => 1}],
|
||||
[video => {'video' => 1, 'playlistItem' => 1}],
|
||||
[subscription => {'subscription' => 1}],
|
||||
@@ -780,23 +796,14 @@ sub get_comments {
|
||||
my ($self, $item) = @_;
|
||||
|
||||
if ($pair->[0] eq 'video') {
|
||||
return 1 if exists $item->{videoId};
|
||||
return 1 if defined $item->{videoId};
|
||||
}
|
||||
|
||||
if ($pair->[0] eq 'playlist') {
|
||||
return 1 if defined $item->{playlistId};
|
||||
}
|
||||
|
||||
exists $pair->[1]{$item->{type} // ''};
|
||||
|
||||
#~ if (ref($item->{id}) eq 'HASH') {
|
||||
#~ if (exists $pair->[1]{$item->{id}{kind}}) {
|
||||
#~ return 1;
|
||||
#~ }
|
||||
#~ }
|
||||
#~ elsif (exists $item->{kind}) {
|
||||
#~ if (exists $pair->[1]{$item->{kind}}) {
|
||||
#~ return 1;
|
||||
#~ }
|
||||
#~ }
|
||||
|
||||
#~ return;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -805,7 +812,7 @@ sub get_comments {
|
||||
sub is_channelID {
|
||||
my ($self, $id) = @_;
|
||||
$id || return;
|
||||
$id eq 'mine' or $id =~ /^UC[-a-zA-Z0-9_]{22}\z/;
|
||||
$id =~ /^UC[-a-zA-Z0-9_]{22}\z/;
|
||||
}
|
||||
|
||||
sub is_videoID {
|
||||
|
||||
@@ -149,7 +149,7 @@ When C<$part> is C<undef>, it defaults to I<snippet>.
|
||||
|
||||
=cut
|
||||
|
||||
sub video_details {
|
||||
sub _invidious_video_details {
|
||||
my ($self, $id, $fields) = @_;
|
||||
|
||||
$fields //= $self->basic_video_info_fields;
|
||||
@@ -159,19 +159,24 @@ sub video_details {
|
||||
return $info;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub video_details {
|
||||
my ($self, $id, $fields) = @_;
|
||||
|
||||
if ($self->get_debug) {
|
||||
say STDERR ":: Extracting video info using the fallback method...";
|
||||
}
|
||||
|
||||
# Fallback using the `get_video_info` URL
|
||||
my %video_info = $self->_get_video_info($id);
|
||||
my $video = $self->parse_json_string($video_info{player_response} // return);
|
||||
my $video = $self->parse_json_string($video_info{player_response} // return $self->_invidious_video_details($id, $fields));
|
||||
|
||||
if (exists $video->{videoDetails}) {
|
||||
$video = $video->{videoDetails};
|
||||
}
|
||||
else {
|
||||
return;
|
||||
return $self->_invidious_video_details($id, $fields);
|
||||
}
|
||||
|
||||
my %details = (
|
||||
@@ -189,7 +194,7 @@ sub video_details {
|
||||
} @{$video->{thumbnail}{thumbnails}}
|
||||
],
|
||||
|
||||
liveNow => $video->{isLiveContent},
|
||||
liveNow => ($video->{isLiveContent} || (($video->{lengthSeconds} || 0) == 0)),
|
||||
description => $video->{shortDescription},
|
||||
lengthSeconds => $video->{lengthSeconds},
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.1
|
||||
<!-- Generated with glade 3.38.1
|
||||
|
||||
Copyright (C) Copyright © 2010-2020 Trizen
|
||||
|
||||
@@ -906,6 +906,7 @@ Author: Trizen https://github.com/trizen
|
||||
<object class="GtkComboBoxText" id="comboboxtext10">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Type of search results</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item translatable="yes">video</item>
|
||||
@@ -959,6 +960,7 @@ Author: Trizen https://github.com/trizen
|
||||
<object class="GtkComboBoxText" id="comboboxtext2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Search order for videos</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item translatable="yes">relevance</item>
|
||||
@@ -1009,7 +1011,6 @@ Author: Trizen https://github.com/trizen
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">short – less than 4 minutes long
|
||||
medium – 4 to 20 minutes (inclusive)
|
||||
long – longer than 20 minutes</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
@@ -1052,6 +1053,7 @@ long – longer than 20 minutes</property>
|
||||
<object class="GtkComboBoxText" id="comboboxtext3">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Retrieve only videos that have closed-captions</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item translatable="yes">any</item>
|
||||
@@ -1092,6 +1094,7 @@ long – longer than 20 minutes</property>
|
||||
<object class="GtkComboBoxText" id="comboboxtext4">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Retrieve only videos with resolution 720p+</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item translatable="yes">any</item>
|
||||
@@ -1117,6 +1120,47 @@ long – longer than 20 minutes</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="frame15">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="alignment14">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="left-padding">12</property>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="comboboxtext5">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Retrieve only videos with Creative-Commons license</property>
|
||||
<property name="active">0</property>
|
||||
<items>
|
||||
<item translatable="yes">any</item>
|
||||
<item translatable="yes">creative_commons</item>
|
||||
</items>
|
||||
<signal name="changed" handler="combobox_license_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="label23">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes"><b>Video License:</b></property>
|
||||
<property name="use-markup">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="more_options_expander">
|
||||
<property name="visible">True</property>
|
||||
@@ -1143,8 +1187,7 @@ long – longer than 20 minutes</property>
|
||||
<object class="GtkSpinButton" id="spinbutton1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="tooltip-text" translatable="yes">The maximum number of results per page.
|
||||
Recommended: 10</property>
|
||||
<property name="tooltip-text" translatable="yes">The maximum number of results per page.</property>
|
||||
<property name="max-length">2</property>
|
||||
<property name="invisible-char">•</property>
|
||||
<property name="caps-lock-warning">False</property>
|
||||
@@ -1239,6 +1282,7 @@ Recommended: 10</property>
|
||||
<object class="GtkComboBoxText" id="comboboxtext1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Retrieve only videos newer than this.</property>
|
||||
<items>
|
||||
<item translatable="yes">anytime</item>
|
||||
<item translatable="yes">hour</item>
|
||||
@@ -1340,6 +1384,7 @@ Unless the author name is valid, this field is ignored.</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Show thumbnails for videos in search results.</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="thumbs_checkbutton_toggled" swapped="no"/>
|
||||
</object>
|
||||
@@ -1355,6 +1400,7 @@ Unless the author name is valid, this field is ignored.</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Start videos in fullscreen mode.</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="toggled_fullscreen" swapped="no"/>
|
||||
</object>
|
||||
@@ -1365,32 +1411,35 @@ Unless the author name is valid, this field is ignored.</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="dash_checkbutton">
|
||||
<property name="label" translatable="yes">DASH support</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="toggled_dash_support" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="audio_only_checkbutton">
|
||||
<property name="label" translatable="yes">Audio only</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Play videos as audio.</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="toggled_audio_only" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="dash_checkbutton">
|
||||
<property name="label" translatable="yes">DASH videos</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Support for videos in DASH format.
|
||||
When disabled, streams in DASH format will be ignored if there exists an alternative.</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="toggled_dash_support" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
@@ -1441,7 +1490,7 @@ Unless the author name is valid, this field is ignored.</property>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">6</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1470,7 +1519,7 @@ Unless the author name is valid, this field is ignored.</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="padding">14</property>
|
||||
<property name="position">7</property>
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -1519,7 +1568,7 @@ When the specified resolution is not found, the best available resolution is use
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">8</property>
|
||||
<property name="position">9</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
Arquivo executável
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
alias perltidy='perltidy -utf8 -l=127 -f -kbl=1 -bbb -bbc -bbs -b -ple -bt=2 -pt=2 -sbt=2 -bvt=0 -sbvt=1 -cti=1 -bar -lp -anl';
|
||||
which perltidy;
|
||||
cd ..;
|
||||
for i in $(git status | grep '^[[:cntrl:]]*modified:' | egrep 'bin/|\.(pm|t)$' | perl -nE 'say +(split)[-1]'); do echo $i; perltidy -b $i; done
|
||||
Arquivo executável
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
for i in $(git status | grep \.bak$ | perl -nE 'say +(split)[-1]'); do echo $i; rm $i; done
|
||||
Referência em uma Nova Issue
Bloquear um usuário