43 Commits

Autor SHA1 Mensagem Data
trizen d10903c9fc Allow setting the video player title to *ID* or *URL* in the configuration file. (https://github.com/trizen/youtube-viewer/issues/348) 2020-11-30 19:58:44 +02:00
trizen 91f7d9594c Filter out private and deleted videos from playlists. 2020-11-28 03:04:43 +02:00
trizen e041b0dd22 Version 0.0.4 2020-11-27 01:14:35 +02:00
trizen d0b99faeeb More entries in the list of invidious instances. 2020-11-21 04:57:47 +02:00
trizen 69df15f4cd Make "publishedText" to include the word "ago". 2020-11-21 04:01:50 +02:00
trizen 14bf3075bd Display the relative age of videos, instead of the publication date (which was inaccurate anyway). 2020-11-21 00:28:38 +02:00
trizen fe37299fb3 weeklyRatio for invidious instances is no longer available. 2020-11-19 15:59:07 +02:00
trizen ec341f6a0e Always set the "liveNow" entry for videos. 2020-11-12 20:43:17 +02:00
trizen 0c1d8bf942 - Fixed live stream videos in search results. (fixes https://github.com/trizen/pipe-viewer/issues/1)
Videos with time == 00:00 are now marked as LIVE videos. Previously, they were ignored, which was a bug.

Thanks to @jharingkaye for reporting the issue.
2020-11-12 20:33:50 +02:00
trizen c3e5e96c2c - Health status for invidious instances is back.
- Minor code tweaks in Utils `has_entries()`.
2020-11-05 15:35:26 +02:00
trizen 245ab8728e Apparently, Invidious no longer includes the status class for instances. 2020-11-05 15:01:47 +02:00
trizen 782b01a022 - Support for YouTube usernames (-u=username).
In addition to channel names and channel IDs.

When `/c/` fails, we now try `/user/`.
2020-11-05 14:32:48 +02:00
trizen 07c0b0146d Version 0.0.3 2020-11-04 22:24:08 +02:00
trizen fb00f6707a Make the invidious URLs on-demand. 2020-11-04 20:58:49 +02:00
trizen 2b980603a6 - Return undef when there are no results.
This allows falling back to invidious.
2020-11-04 20:48:31 +02:00
trizen 6b9f69d3c4 - Stricter selection of ividious instances.
When `api_host => "auto"`, try to pick a good invidious instance that has an overall "success" status.
2020-11-04 18:38:36 +02:00
trizen 03b8155000 Fixed the "odd number of elements in hash" warning when there are no results. 2020-11-04 18:31:55 +02:00
trizen a60ec93bef - Added support for next pages in --author context. 2020-11-04 18:20:19 +02:00
trizen b7b6237142 Don't cache if "cache-control" says "no-cache". 2020-11-04 02:10:48 +02:00
trizen 984b124f9b - Implemented support for searching for videos from a given channel.
Example:

	pipe-viewer --author=DistroTube ubuntu

equivalently, given a channel ID:

	pipe-viewer --author=UCVls1GmFKf6WlTraIb_IaJg ubuntu

Fixes the following issue in straw-viewer: https://github.com/trizen/straw-viewer/issues/34

- Better finding of the sectionList, without depending on the specific tab number.
2020-11-04 01:33:17 +02:00
trizen fa2af6de56 - Added support for multiple search parameters.
Example:

	# Long "linux" videos published last week
	pipe-viewer --date=week --duration=long linux
2020-11-03 19:23:15 +02:00
trizen 9195d7bb4a - Fixed the parsing of numbers like "3.5K" and "12.5M".
- Added `:i=n` STDIN option for channels.
- Use "hl=en" in URLs.
- Display the number of subscribers in channel search results (instead of video count).
2020-11-02 14:41:07 +02:00
trizen fab941b13b Version 0.0.2 2020-11-01 20:03:52 +02:00
trizen 7b901b85a8 - Added support for next pages.
This was hard... :-)
2020-11-01 19:44:43 +02:00
trizen 69cfe03047 - With api_host => "auto", try to pick an invidious instance with "monitor" enabled that has "statusClass = success". 2020-11-01 01:12:23 +02:00
trizen 48ae1101e4 Better quality for playlist thumbnails. 2020-11-01 00:54:56 +02:00
trizen 243f5fa231 - Implemented support for listing the most popular videos from a channel. 2020-11-01 00:32:32 +02:00
trizen ad4e60ae82 - Implemented support for searching playlists and channels.
- Implemented support for search filtering. (currently, only one filter per search)
- Display the number of videos for channel and playlist results.
2020-11-01 00:19:58 +02:00
trizen 64c64c821d Implemented support for listing a playlist of videos by playlistID. 2020-10-31 21:46:13 +02:00
trizen 565dcfd83c - Extended right-click "Play as audio" to support playlists. 2020-10-31 18:16:03 +02:00
trizen 9ac917318c - Implemented support for playlists from a given channelID or username.
Example:

	pipe-viewer -up=google
2020-10-31 18:06:09 +02:00
trizen f52110ae5f Hide the non-working right-click "Author->Llikes" and "Author->Activity" entries. 2020-10-31 14:03:33 +02:00
trizen 7ae7550410 Allow the username in YouTube URLs to contain encoded characters of the form "%FF". 2020-10-31 13:14:25 +02:00
trizen 5360ca0053 Support channel URLs of the form "https://www.youtube.com/c/USERNAME/videos" 2020-10-31 13:09:46 +02:00
trizen 3347b0737e - Renamed config-option "dash" to "split_videos".
- Renamed config-option "dash_segmented" to "dash".
- Renamed config-option "dash_mp4_audio" to "m4a_audio".
2020-10-31 12:50:30 +02:00
trizen 80010c63ba Implemented support for uploads from channel (-u=channelID and :a=i) 2020-10-31 00:57:06 +02:00
trizen c4155faa88 Fallback to invidious when failing to extract video info. 2020-10-30 22:45:43 +02:00
trizen c1025bb669 Fixed the "TRY" section. 2020-10-30 22:29:10 +02:00
trizen 71db0b2c14 - Extract video info using the fallback method. 2020-10-30 22:01:43 +02:00
trizen ad3a191c07 gtk: pass "--no-dash-segmented" to the CLI version when dash_segmented is disabled.
gtk: added a few more tooltips in the GUI.
2020-10-30 20:41:15 +02:00
trizen 6ec35f1dfb modified: README.md -- updated screenshot for CLI version 2020-10-30 19:42:25 +02:00
trizen 95bd129ad4 gtk: added the "Video License" combobox.
Allows filtering videos by license in search results.

- Minor internal fixes.
2020-10-30 19:28:01 +02:00
trizen 47f284e38b new file: META.json
new file:   META.yml
2020-10-30 18:37:15 +02:00
24 arquivos alterados com 1753 adições e 615 exclusões
+3 -2
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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.
+3
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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.
![pipe-viewer](https://user-images.githubusercontent.com/614513/73046877-5cae1200-3e7c-11ea-8ab3-f8c444f88b30.png)
![pipe-viewer](https://user-images.githubusercontent.com/614513/97738550-6d0faf00-1ad6-11eb-84ec-d37f28073d9d.png)
### gtk-pipe-viewer
* GTK+ interface to YouTube.
![gtk-pipe-viewer](https://user-images.githubusercontent.com/614513/84770876-11d69780-afe1-11ea-96f7-5d426dc865e5.png)
![gtk-pipe-viewer](https://user-images.githubusercontent.com/614513/97737137-89125100-1ad4-11eb-8ff3-b19cd0041528.png)
### 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
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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);
}
+4 -4
Ver Arquivo
@@ -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,
]
+14 -3
Ver Arquivo
@@ -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)
+1 -5
Ver Arquivo
@@ -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)
+882
Ver Arquivo
@@ -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
+77 -74
Ver Arquivo
@@ -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
+2 -2
Ver Arquivo
@@ -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;
}
+7 -1
Ver Arquivo
@@ -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)
+8 -5
Ver Arquivo
@@ -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)
+2 -2
Ver Arquivo
@@ -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
Ver Arquivo
@@ -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 => []};
+92 -85
Ver Arquivo
@@ -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 {
+10 -5
Ver Arquivo
@@ -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},
+71 -22
Ver Arquivo
@@ -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">&lt;b&gt;Video License:&lt;/b&gt;</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
Ver Arquivo
@@ -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
Ver Arquivo
@@ -0,0 +1,3 @@
#!/bin/sh
for i in $(git status | grep \.bak$ | perl -nE 'say +(split)[-1]'); do echo $i; rm $i; done