package YTMusicAPI::Continuations; use strict; use warnings; use Exporter 'import'; use YTMusicAPI::Navigation; sub get_continuations { my ( $results, $continuation_type, $limit, $request_func, $parse_func, $ctoken_path, $reloadable ) = @_; $ctoken_path //= ""; $reloadable //= 0; my @items = (); while ( exists $results->{"continuations"} && ( !defined $limit || scalar(@items) < $limit ) ) { my $additionalParams = $reloadable ? get_reloadable_continuation_params($results) : get_continuation_params( $results, $ctoken_path ); my $response = $request_func->($additionalParams); if ( exists $response->{"continuationContents"} ) { $results = $response->{"continuationContents"}->{$continuation_type}; } else { last; } my $contents = get_continuation_contents( $results, $parse_func ); last if scalar(@$contents) == 0; push( @items, @$contents ); } return \@items; } sub get_validated_continuations { my ( $results, $continuation_type, $limit, $per_page, $request_func, $parse_func, $ctoken_path ) = @_; $ctoken_path //= ""; my @items = (); while ( exists $results->{"continuations"} && scalar(@items) < $limit ) { my $additionalParams = get_continuation_params( $results, $ctoken_path ); my $wrapped_parse_func = sub { my $raw_response = shift; return get_parsed_continuation_items( $raw_response, $parse_func, $continuation_type ); }; my $validate_func = sub { my $parsed = shift; return validate_response( $parsed, $per_page, $limit, scalar(@items) ); }; my $response = resend_request_until_parsed_response_is_valid( $request_func, $additionalParams, $wrapped_parse_func, $validate_func, 3 ); $results = $response->{"results"}; push( @items, @{ $response->{"parsed"} } ); } return \@items; } sub get_parsed_continuation_items { my ( $response, $parse_func, $continuation_type ) = @_; my $results = $response->{"continuationContents"}->{$continuation_type}; return { "results" => $results, "parsed" => get_continuation_contents( $results, $parse_func ) }; } sub get_continuation_params { my ( $results, $ctoken_path ) = @_; $ctoken_path //= ""; my $ctoken = nav( $results, [ "continuations", 0, "next" . $ctoken_path . "ContinuationData", "continuation" ] ); return get_continuation_string($ctoken); } sub get_reloadable_continuation_params { my ($results) = @_; my $ctoken = nav( $results, [ "continuations", 0, "reloadContinuationData", "continuation" ] ); return get_continuation_string($ctoken); } sub get_continuation_string { my ($ctoken) = @_; return "&ctoken=" . $ctoken . "&continuation=" . $ctoken; } sub get_continuation_contents { my ( $continuation, $parse_func ) = @_; foreach my $term ( "contents", "items" ) { if ( exists $continuation->{$term} ) { return $parse_func->( $continuation->{$term} ); } } return []; } sub resend_request_until_parsed_response_is_valid { my ( $request_func, $request_additional_params, $parse_func, $validate_func, $max_retries ) = @_; my $response = $request_func->($request_additional_params); my $parsed_object = $parse_func->($response); my $retry_counter = 0; while ( !$validate_func->($parsed_object) && $retry_counter < $max_retries ) { $response = $request_func->($request_additional_params); my $attempt = $parse_func->($response); if ( scalar( @{ $attempt->{"parsed"} } ) > scalar( @{ $parsed_object->{"parsed"} } ) ) { $parsed_object = $attempt; } $retry_counter++; } return $parsed_object; } sub validate_response { my ( $response, $per_page, $limit, $current_count ) = @_; my $remaining_items_count = $limit - $current_count; my $expected_items_count = $per_page < $remaining_items_count ? $per_page : $remaining_items_count; # response is invalid, if it has less items than the minimal expected count return scalar( @{ $response->{"parsed"} } ) >= $expected_items_count; } our @EXPORT = qw( get_continuations get_validated_continuations get_parsed_continuation_items get_continuation_params get_reloadable_continuation_params get_continuation_string get_continuation_contents resend_request_until_parsed_response_is_valid validate_response ); 1;