Package: release.debian.org Severity: normal User: release.debian....@packages.debian.org Usertags: unblock
Hi, Please unblock get-iplayer/2.87-2. Sorry that this is a little late up to the starting gate. get-iplayer is for browsing the BBC iPlayer archives with free software. It consumes various feeds and can display schedules, download programmes and stream live data. A few weeks ago the BBC radically changed the way the feeds are produced, rendering get-iplayer effectively useless. Upstream has made a number of changes to suit, of which I've cherry picked the ones that restore basic functionality, but left out nicities like thumbnail substitution. The patches are unforunately invasive but necessary. The bug fixed is severity:important. Without an unblock, the alternative is to remove get-iplayer altogether. My intention is to ship this working version in Jessie, and augment it with the other missing features in backports later. Debdiff attached. changelog | 8 patches/cache-search.patch | 1030 ++++++++++++++++++++++++++++++++++++++++++ patches/data-feeds.patch | 221 +++++++++ patches/future-schedule.patch | 81 +++ patches/live-tv.patch | 18 patches/series | 4 6 files changed, 1362 insertions(+) -- Jonathan Wiltshire j...@debian.org Debian Developer http://people.debian.org/~jmw 4096R: 0xD3524C51 / 0A55 B7C5 1223 3942 86EC 74C3 5394 479D D352 4C51
diff -Nru get-iplayer-2.87/debian/changelog get-iplayer-2.87/debian/changelog --- get-iplayer-2.87/debian/changelog 2014-10-20 20:39:26.000000000 +0100 +++ get-iplayer-2.87/debian/changelog 2014-12-04 23:22:09.000000000 +0000 @@ -1,3 +1,11 @@ +get-iplayer (2.87-2) unstable; urgency=medium + + * Fix feed fetch, parse and search across all channels after invasive + changes by the BBC. All patches cherry-picked from upstream + (Closes: #767331) + + -- Jonathan Wiltshire <j...@debian.org> Thu, 04 Dec 2014 23:21:16 +0000 + get-iplayer (2.87-1) unstable; urgency=medium * New upstream release (Closes: #743905, 623179) diff -Nru get-iplayer-2.87/debian/patches/cache-search.patch get-iplayer-2.87/debian/patches/cache-search.patch --- get-iplayer-2.87/debian/patches/cache-search.patch 1970-01-01 01:00:00.000000000 +0100 +++ get-iplayer-2.87/debian/patches/cache-search.patch 2014-12-04 23:22:09.000000000 +0000 @@ -0,0 +1,1030 @@ +From: dinkypumpkin <dinkypump...@gmail.com> +Date: Thu, 30 Oct 2014 21:54:17 +0000 +Subject: Partial restoration of cache and search functions +Origin: http://git.infradead.org/get_iplayer.git/commit/8220a6bf04a83f49f75d4996b6222ef3fd5d9d8b + http://git.infradead.org/get_iplayer.git/commit/0c1e7ab3fb92656613dfe7811a31d559224957cc +Last-Update: 2014-12-03 + +--- get-iplayer-2.87.orig/get_iplayer ++++ get-iplayer-2.87/get_iplayer +@@ -227,6 +227,12 @@ + refreshinclude => [ 1, "refreshinclude|refresh-include=s", 'Config', '--refresh-include <string>', "Include matched channel(s) when refreshing cache (regex or comma separated values)"], + refreshexclude => [ 1, "refreshexclude|refresh-exclude|ignorechannels=s", 'Config', '--refresh-exclude <string>', "Exclude matched channel(s) when refreshing cache (regex or comma separated values)"], + refreshfuture => [ 1, "refreshfuture|refresh-future!", 'Config', '--refresh-future', "Obtain future programme schedule when refreshing cache (between 7-14 days)"], ++ refreshfeeds => [ 1, "refreshfeeds|refresh-feeds=s", 'Config', '--refresh-feeds <string>', "Alternate source for programme data. Valid values: 'schedule'"], ++ refreshfeedsradio => [ 1, "refreshfeedsradio|refresh-feeds-radio=s", 'Config', '--refresh-feeds-radio <string>', "Alternate source for radio programme data. Valid values: 'schedule'"], ++ refreshfeedstv => [ 1, "refreshfeedstv|refresh-feeds-tv=s", 'Config', '--refresh-feeds-tv <string>', "Alternate source for TV programme data. Valid values: 'schedule'"], ++ refreshlimit => [ 1, "refreshlimit|refresh-limit=n", 'Config', '--refresh-limit <integer>', "Number of days of programmes to cache. Only applied with --refresh-feeds=schedule. Makes cache updates VERY slow. Default: 7 Min: 1 Max: 30"], ++ refreshlimitradio => [ 1, "refreshlimitradio|refresh-limit-radio=n", 'Config', '--refresh-limit-radio <integer>', "Number of days of radio programmes to cache. Only applied with --refresh-feeds=schedule. Makes cache updates VERY slow. Default: 7 Min: 1 Max: 30"], ++ refreshlimittv => [ 1, "refreshlimittv|refresh-limit-tv=n", 'Config', '--refresh-limit-tv <integer>', "Number of days of TV programmes to cache. Only applied with --refresh-feeds=schedule. Makes cache updates VERY slow. Default: 7 Min: 1 Max: 30"], + skipdeleted => [ 1, "skipdeleted!", 'Config', "--skipdeleted", "Skip the download of metadata/thumbs/subs if the media file no longer exists. Use with --history & --metadataonly/subsonly/thumbonly."], + update => [ 2, "update|u!", 'Config', '--update, -u', "Update get_iplayer if a newer one exists"], + webrequest => [ 1, "webrequest=s", 'Config', '--webrequest <urlencoded string>', 'Specify all options as a urlencoded string of "name=val&name=val&..."' ], +@@ -6996,7 +7002,7 @@ + use IO::Socket; + use LWP::ConnCache; + use LWP::UserAgent; +-use POSIX qw(mkfifo); ++use POSIX qw(mkfifo strftime); + use strict; + use Time::Local; + use URI; +@@ -7009,24 +7015,16 @@ + sub index_max { return 9999 } + sub channels { + return { +- 'bbcone' => 'BBC One', +- 'bbctwo' => 'BBC Two', +- 'bbcthree' => 'BBC Three', +- 'bbcfour' => 'BBC Four', +- 'bbcnews' => 'BBC News', +- 'bbcnews24' => 'BBC News', ++ 'bbc_one' => 'BBC One', ++ 'bbc_two' => 'BBC Two', ++ 'bbc_three' => 'BBC Three', ++ 'bbc_four' => 'BBC Four', + 'cbbc' => 'CBBC', + 'cbeebies' => 'CBeebies', +- 'parliament' => 'BBC Parliament', +- 'bbcwebonly' => 'BBC Web Only', +- 'bbchd' => 'BBC HD', +- 'bbcalba' => 'BBC Alba', +- 'categories/news/tv' => 'BBC News', +- 'categories/sport/tv' => 'BBC Sport', +- 'categories/signed' => 'Signed', +- 'categories/audiodescribed' => 'Audio Described', +- 'popular/tv' => 'Popular', +- 'highlights/tv' => 'Highlights', ++ 'bbc_news24' => 'BBC News', ++ 'bbc_parliament' => 'BBC Parliament', ++ 'bbc_alba' => 'BBC Alba', ++ 'bbc_webonly' => 'BBC Web Only', + }; + } + +@@ -7176,143 +7174,53 @@ + } + } + +- +- +-# Usage: Programme::tv->get_links( \%prog, 'tv' ); +-# Uses: %{ channels() }, \%prog +-sub get_links { +- shift; # ignore obj ref ++sub get_links_aod { ++ my $self = shift; + my $prog = shift; + my $prog_type = shift; ++ return 1 if $prog_type ne "radio"; + # Hack to get correct 'channels' method because this methods is being shared with Programme::radio +- my %channels = %{ main::progclass($prog_type)->channels_filtered( main::progclass($prog_type)->channels() ) }; +- my $channel_feed_url = 'http://feeds.bbc.co.uk/iplayer'; # /$channel/list ++ my %channels = %{ main::progclass($prog_type)->channels_filtered( main::progclass($prog_type)->channels_aod() ) }; + my $bbc_prog_page_prefix = 'http://www.bbc.co.uk/programmes'; # /$pid +- my $thumbnail_prefix = 'http://www.bbc.co.uk/iplayer/images/episode'; +- my $xml; +- my $feed_data; +- my $res; +- main::logger "INFO: Getting $prog_type Index Feeds\n"; + # Setup User agent + my $ua = main::create_ua( 'desktop', 1 ); +- + # Download index feed +- # Sort feeds so that category based feeds are done last - this makes sure that the channels get defined correctly if there are dups +- my @channel_list; +- push @channel_list, grep !/(categor|popular|highlights|bbchd)/, keys %channels; +- push @channel_list, grep /categor/, keys %channels; +- push @channel_list, grep /popular/, keys %channels; +- push @channel_list, grep /highlights/, keys %channels; +- push @channel_list, grep /bbchd/, keys %channels; +- for ( @channel_list ) { +- +- my $url = "${channel_feed_url}/$_/list/limit/400"; +- main::logger "DEBUG: Getting feed $url\n" if $opt->{verbose}; +- $xml = main::request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get programme index feed for $_ from iplayer site\n"); ++ my @channel_list = keys %channels; ++ for my $channel_id ( @channel_list ) { ++ my $url = "http://www.bbc.co.uk/radio/aod/availability/${channel_id}.xml"; ++ main::logger "\nDEBUG: Getting feed $url\n" if $opt->{verbose}; ++ my $xml = main::request_url_retry($ua, $url, 3, '.', "\nWARNING: Failed to get programme index feed for $channels{$channel_id}\n"); + decode_entities($xml); +- +- # Feed as of August 2008 +- # <entry> +- # <title type="text">Bargain Hunt: Series 18: Oswestry</title> +- # <id>tag:feeds.bbc.co.uk,2008:PIPS:b0088jgs</id> +- # <updated>2008-07-22T00:23:50Z</updated> +- # <content type="html"> +- # <p> +- # <a href="http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30"> +- # <img src="http://www.bbc.co.uk/iplayer/images/episode/b0088jgs_150_84.jpg" alt="Bargain Hunt: Series 18: Oswestry" /> +- # </a> +- # </p> +- # <p> +- # The teams are at an antiques fair in Oswestry showground. Hosted by Tim Wonnacott. +- # </p> +- # </content> +- # <category term="Factual" /> +- # <category term="Guidance" /> +- # <category term="TV" /> +- # <link rel="via" href="http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30" type="text/html" title="Bargain Hunt: Series 18: Oswestry" /> +- # </entry> +- # +- +- ### New Feed +- # <entry> +- # <title type="text">House of Lords: 02/07/2008</title> +- # <id>tag:bbc.co.uk,2008:PIPS:b00cd5p7</id> +- # <updated>2008-06-24T00:15:11Z</updated> +- # <content type="html"> +- # <p> +- # <a href="http://www.bbc.co.uk/iplayer/episode/b00cd5p7?src=a_syn30"> +- # <img src="http://www.bbc.co.uk/iplayer/images/episode/b00cd5p7_150_84.jpg" alt="House of Lords: 02/07/2008" /> +- # </a> +- # </p> +- # <p> +- # House of Lords, including the third reading of the Health and Social Care Bill. 1 July. +- # </p> +- # </content> +- # <category term="Factual" scheme="urn:bbciplayer:category" /> +- # <link rel="via" href="http://www.bbc.co.uk/iplayer/episode/b00cd5p7?src=a_syn30" type="application/atom+xml" title="House of Lords: 02/07/2008"> +- # </link> +- # </entry> +- +- ### Newer feed (Sept 2009) +- # <entry> +- # <title type="text">BBC Proms: 2009: Prom 65: Gustav Mahler Jugend Orchester</title> +- # <id>tag:feeds.bbc.co.uk,2008:PIPS:b00mgw03</id> +- # <updated>2009-09-05T03:29:07Z</updated> +- # <content type="html"> +- # <p> +- # <a href="http://www.bbc.co.uk/iplayer/episode/b00mgw03/BBC_Proms_2009_Prom_65_Gustav_Mahler_Jugend_Orchester/"> +- # <img src="http://node1.bbcimg.co.uk/iplayer/images/episode/b00mgw03_150_84.jpg" alt="BBC Proms: 2009: Prom 65: Gustav Mahler Jugend Orchester" /> +- # </a> +- # </p> +- # <p> +- # The Gustav Mahler Youth Orchestra perform works by Mahler, Richard Strauss and Ligeti. +- # </p> +- # </content> +- # <category term="Music" /> +- # <category term="Classical" /> +- # <category term="TV" /> +- # <link rel="alternate" href="http://www.bbc.co.uk/iplayer/episode/b00mgw03/BBC_Proms_2009_Prom_65_Gustav_Mahler_Jugend_Orchester/" type="text/html" title="BBC Proms: 2009: Prom 65: Gustav Mahler Jugend Orchester"> +- # <media:content> +- # <media:thumbnail url="http://node1.bbcimg.co.uk/iplayer/images/episode/b00mgw03_150_84.jpg" width="150" height="84" /> +- # </media:content> +- # </link> +- # <link rel="self" href="http://feeds.bbc.co.uk/iplayer/episode/b00mgw03" type="application/atom+xml" title="Prom 65: Gustav Mahler Jugend Orchester" /> +- # <link rel="related" href="http://www.bbc.co.uk/programmes/b007v097/microsite" type="text/html" title="BBC Proms" /> +- # </entry> +- +- + # Parse XML +- + # get list of entries within <entry> </entry> tags +- my @entries = split /<entry>/, $xml; ++ my @entries = split /<entry/, $xml; + # Discard first element == header + shift @entries; +- +- main::logger "INFO: Got ".($#entries + 1)." programmes\n" if $opt->{verbose}; ++ main::logger "\nINFO: Got ".($#entries + 1)." programmes for $channels{$channel_id}\n" if $opt->{verbose}; ++ my $now = time(); + foreach my $entry (@entries) { + my ( $title, $name, $episode, $episodetitle, $nametitle, $episodenum, $seriesnum, $desc, $pid, $available, $channel, $duration, $thumbnail, $version, $guidance ); +- +- my $entry_flat = $entry; +- $entry_flat =~ s/\n/ /g; +- +- # <id>tag:bbc.co.uk,2008:PIPS:b008pj3w</id> +- $pid = $1 if $entry =~ m{<id>.*PIPS:(.+?)</id>}; +- +- # <title type="text">Richard Hammond's Blast Lab: Series Two: Episode 11</title> +- # <title type="text">Skate Nation: Pro-Skate Camp</title> +- $title = $1 if $entry =~ m{<title\s*.*?>\s*(.*?)\s*</title>}; +- ++ my ($start, $available) = ($1, $2) if $entry =~ m{<availability\s+start="(.*?)"\s+end="(.*?)"}; ++ next if ! ( $start || $available ); ++ if ( $start ) { ++ my $xstart = Programme::get_time_string( $start ); ++ next if $xstart > $now; ++ } ++ if ( $available ) { ++ my $xavailable = Programme::get_time_string( $available ); ++ next if $xavailable < $now; ++ } ++ my $vpid = $1 if $entry =~ m{pid="(.+?)">}; ++ $pid = $1 if $entry =~ m{<pid>(.+?)</pid>}; ++ $duration = $1 if $entry =~ m{duration="(.*?)"}; ++ $title = $1 if $entry =~ m{<title>(.*?)</title>}; + # determine name and episode from title + ( $name, $episode ) = Programme::bbciplayer::split_title( $title ); +- +- # Get the title from the atom link refs only to determine the longer episode name +- $episodetitle = $1 if $entry =~ m{<link\s+rel="self"\s+href="http.+?/episode/.+?"\s+type="application/atom\+xml"\s+title="(.+?)"}; +- $nametitle = $1 if $entry =~ m{<link\s+rel="related"\s+href="http.+?/programmes/.+?"\s+type="text/html"\s+title="(.+?)"}; +- ++ $episodetitle = $episode; ++ $nametitle = $name; + # Extract the seriesnum + my $regex = 'Series\s+'.main::regex_numbers(); + $seriesnum = main::convert_words_to_number( $1 ) if "$name $episode" =~ m{$regex}i; +- + # Extract the episode num + my $regex_1 = 'Episode\s+'.main::regex_numbers(); + my $regex_2 = '^'.main::regex_numbers().'\.\s+'; +@@ -7323,79 +7231,20 @@ + } elsif ( $episodetitle =~ m{$regex_2}i ) { + $episodenum = main::convert_words_to_number( $1 ); + } +- + # Re-insert the episode number if the episode text doesn't have it + if ( $episodenum && $episodetitle =~ /^\d+\./ && $episode !~ /^(.+:\s+)?\d+\./ ) { + $episode =~ s/^(.+:\s+)?(.*)$/$1$episodenum. $2/; + } +- +- #<p> House of Lords, including the third reading of the Health and Social Care Bill. 1 July. </p> </content> +- $desc = $1 if $entry =~ m{<p>\s*(.*?)\s*</p>\s*</content>}; +- $desc =~ s|[\n\r]| |g; +- # Remove unwanted html tags +- $desc =~ s!</?(br|b|i|p|strong)\s*/?>!!gi; +- +- # Parse the categories into hash +- # <category term="Factual" /> +- my @category; +- for my $line ( grep /<category/, (split /\n/, $entry) ) { +- push @category, $1 if $line =~ m{<category\s+term="(.+?)"}; +- } +- # strip commas - they confuse sorting and spliting later +- s/,//g for @category; +- ++ $desc = $1 if $entry =~ m{<synopsis>(.*?)</synopsis>}; + # Extract channel +- $channel = $channels{$_}; +- # Add HD as category +- push @category, 'HD' if $channel eq 'BBC HD'; +- ++ $channel = $channels{$channel_id}; + main::logger "DEBUG: '$pid, $name - $episode, $channel'\n" if $opt->{debug}; +- + # Merge and Skip if this pid is a duplicate + if ( defined $prog->{$pid} ) { +- main::logger "WARNING: '$pid, $prog->{$pid}->{name} - $prog->{$pid}->{episode}, $prog->{$pid}->{channel}' already exists (this channel = $channel)\n" if $opt->{verbose}; +- # Since we use the 'Signed' (or 'Audio Described') channel to get sign zone/audio described data, merge the categories from this entry to the existing entry +- if ( $prog->{$pid}->{categories} ne join(',', sort @category) ) { +- my %cats; +- $cats{$_} = 1 for ( @category, split /,/, $prog->{$pid}->{categories} ); +- main::logger "INFO: Merged categories for $pid from $prog->{$pid}->{categories} to ".join(',', sort keys %cats)."\n" if $opt->{verbose}; +- $prog->{$pid}->{categories} = join(',', sort keys %cats); +- } +- +- # If this a popular or highlights programme then add these tags to categories +- my %cats; +- $cats{$_} = 1 for ( @category, split /,/, $prog->{$pid}->{categories} ); +- $cats{Popular} = 1 if $channel eq 'Popular'; +- $cats{Highlights} = 1 if $channel eq 'Highlights'; +- $prog->{$pid}->{categories} = join(',', sort keys %cats); +- +- # If this is a dupicate pid and the channel is now Signed then both versions are available +- $version = 'signed' if $channel eq 'Signed'; +- $version = 'audiodescribed' if $channel eq 'Audio Described'; +- # Add version to versions for existing prog +- $prog->{$pid}->{versions} = join ',', main::make_array_unique_ordered( (split /,/, $prog->{$pid}->{versions}), $version ); + next; + } +- +- # Set guidance based on category +- $guidance = 'Yes' if grep /guidance/i, @category; +- +- # Check for signed-only or audiodescribed-only version from Channel +- if ( $channel eq 'Signed' ) { +- $version = 'signed'; +- } elsif ( $channel eq 'Audio Described' ) { +- $version = 'audiodescribed'; +- } else { +- $version = 'default'; +- } +- +- # Default to 150px width thumbnail; +- my $thumbsize = $opt->{thumbsizecache} || 150; +- my $thumbnail = $1 if $entry =~ m{<media:thumbnail.*?url="(.*?)"}; +- my $suffix = Programme::bbciplayer->thumb_url_suffixes->{ $thumbsize }; +- if ( $thumbnail !~ /$suffix/ ) { +- $thumbnail =~ s/_\d+_\d+\.jpg/$suffix/; +- } ++ $version = 'default'; ++ my @category; + # build data structure + $prog->{$pid} = main::progclass($prog_type)->new( + 'pid' => $pid, +@@ -7407,203 +7256,408 @@ + 'desc' => $desc, + 'guidance' => $guidance, + 'available' => 'Unknown', +- 'duration' => 'Unknown', ++ 'duration' => $duration || 'Unknown', + 'thumbnail' => $thumbnail, + 'channel' => $channel, + 'categories' => join(',', sort @category), + 'type' => $prog_type, +- 'web' => "${bbc_prog_page_prefix}/${pid}.html", ++ 'web' => "${bbc_prog_page_prefix}/${pid}", + ); +- + } + } ++} + +- # Get future schedules if required +- # http://www.bbc.co.uk/cbbc/programmes/schedules/this_week.xml +- # http://www.bbc.co.uk/cbbc/programmes/schedules/next_week.xml +- if ( $opt->{refreshfuture} ) { +- # Hack to get correct 'channels' method because this methods is being shared with Programme::radio +- my %channels = %{ main::progclass($prog_type)->channels_filtered( main::progclass($prog_type)->channels_schedule() ) }; +- # Only get schedules for real channels +- @channel_list = keys %channels; +- for my $channel_id ( @channel_list ) { +- my @schedule_feeds = ( +- "http://www.bbc.co.uk/${channel_id}/this_week.xml", +- "http://www.bbc.co.uk/${channel_id}/next_week.xml", +- ); +- for my $url ( @schedule_feeds ) { +- main::logger "DEBUG: Getting feed $url\n" if $opt->{verbose}; +- $xml = main::request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get programme schedule feed for $channel_id from iplayer site\n"); +- decode_entities($xml); +- +- # <broadcast is_repeat="1" is_blanked="0"> +- # <pid>p00l44r8</pid> +- # <start>2011-10-24T00:45:00+01:00</start> +- # <end>2011-10-24T01:15:00+01:00</end> +- # <duration>1800</duration> +- # <programme type="episode"> +- # <pid>b016c73c</pid> +- # <position>3</position> +- # <title>Episode 3</title> +- # <short_synopsis>With team captains Noel Fielding and Phill Jupitus, and a surprise special guest host.</short_synopsis> +- # <media_type>audio_video</media_type> +- # <duration>2100</duration> +- # <display_titles> +- # <title>Never Mind the Buzzcocks</title> +- # <subtitle>Series 25, Episode 3</subtitle> +- # </display_titles> +- # <first_broadcast_date>2011-10-17T22:00:00+01:00</first_broadcast_date> +- # <ownership> +- # <service type="tv" id="bbc_two" key="bbctwo"> +- # <title>BBC Two</title> +- # </service> +- # </ownership> +- # <programme type="series"> +- # <pid>b015skhy</pid> +- # <title>Series 25</title> +- # <position>25</position> +- # <expected_child_count>12</expected_child_count> +- # <first_broadcast_date>2011-10-03T22:00:00+01:00</first_broadcast_date> +- # <programme type="brand"> +- # <pid>b006v0dz</pid> +- # <title>Never Mind the Buzzcocks</title> +- # <position /> +- # <expected_child_count /> +- # <first_broadcast_date>2007-06-20T22:00:00+01:00</first_broadcast_date> +- # <ownership> +- # <service type="tv" id="bbc_two" key="bbctwo"> +- # <title>BBC Two</title> +- # </service> +- # </ownership> +- # </programme> +- # </programme> +- # <available_until>2011-10-30T23:29:00Z</available_until> +- # <actual_start>2011-10-17T22:30:00+01:00</actual_start> +- # <is_available_mediaset_pc_sd>1</is_available_mediaset_pc_sd> +- # <is_legacy_media>0</is_legacy_media> +- # <media format="video"> +- # <expires>2011-10-30T23:29:00Z</expires> +- # <availability>11 days left to watch</availability> +- # </media> +- # </programme> +- # </broadcast> +- +- # get list of entries within <broadcast> </broadcast> tags +- my @entries = split /<broadcast[^s]/, $xml; +- # Discard first element == header +- shift @entries; +- main::logger "INFO: Got ".($#entries + 1)." programmes\n" if $opt->{verbose}; +- my $now = time(); +- foreach my $entry (@entries) { +- my ( $title, $channel, $name, $episode, $episodetitle, $nametitle, $seriestitle, $episodenum, $seriesnum, $desc, $pid, $available, $duration, $thumbnail, $version, $guidance ); +- +- my $entry_flat = $entry; +- $entry_flat =~ s/\n/ /g; +- +- $pid = $1 if $entry =~ m{<programme\s+type="episode">.*?<pid>\s*(.+?)\s*</pid>}; +- +- $episode = $1 if $entry =~ m{<programme\s+type="episode">.*?<title>\s*(.*?)\s*</title>}; +- $nametitle = $1 if $entry =~ m{<programme\s+type="brand">.*?<title>\s*(.*?)\s*</title>.*?</programme>}; +- $seriestitle = $1 if $entry =~ m{<programme\s+type="series">.*?<title>\s*(.*?)\s*</title>.*?</programme>}; +- +- # Set name +- if ( $nametitle && $seriestitle ) { +- $name = "$nametitle: $seriestitle"; +- } elsif ( $seriestitle && ! $nametitle ) { +- $name = $seriestitle; +- # Fallback to episade name if the BBC missed out both Series and Name +- } elsif ( ( ! $seriestitle ) && ! $nametitle ) { +- $name = $episode; +- } else { +- $name = $nametitle; +- } +- +- # Extract the seriesnum +- my $regex = 'Series\s+'.main::regex_numbers(); +- $seriesnum = main::convert_words_to_number( $1 ) if $seriestitle =~ m{$regex}i; +- +- # Extract the episode num +- my $regex_1 = 'Episode\s+'.main::regex_numbers(); +- my $regex_2 = '^'.main::regex_numbers().'\.\s+'; +- if ( $episode =~ m{$regex_1}i ) { +- $episodenum = main::convert_words_to_number( $1 ); +- } elsif ( $episode =~ m{$regex_2}i ) { +- $episodenum = main::convert_words_to_number( $1 ); +- } + +- # extract desc +- if ( $entry =~ m{<long_synopsis>\s*(.+?)\s*</long_synopsis>} ) { +- $desc = $1; +- } elsif ( $entry =~ m{<medium_synopsis>\s*(.+?)\s*</medium_synopsis>} ) { +- $desc = $1; +- } elsif ( $entry =~ m{<short_synopsis>\s*(.+?)\s*</short_synopsis>} ) { +- $desc = $1; +- }; +- # Remove unwanted html tags +- $desc =~ s!</?(br|b|i|p|strong)\s*/?>!!gi; +- +- $duration = $1 if $entry =~ m{<duration>\s*(.+?)\s*</duration>}; +- $available = $1 if $entry =~ m{<start>\s*(.+?)\s*</start>}; +- +- # Extract channel nice name +- $channel = $channels{$channel_id}; +- +- main::logger "DEBUG: '$pid, $name - $episode, $channel'\n" if $opt->{debug}; +- +- # Merge and Skip if this pid is a duplicate +- if ( defined $prog->{$pid} ) { +- main::logger "WARNING: '$pid, $prog->{$pid}->{name} - $prog->{$pid}->{episode}, $prog->{$pid}->{channel}' already exists (this channel = $channel)\n" if $opt->{verbose}; +- # Update this info from schedule (not available in the usual iplayer channels feeds) +- $prog->{$pid}->{duration} = $duration; +- $prog->{$pid}->{episodenum} = $episodenum if ! $prog->{$pid}->{episodenum}; +- $prog->{$pid}->{seriesnum} = $seriesnum if ! $prog->{$pid}->{seriesnum}; +- # don't add this as some progs are already available +- #$prog->{$pid}->{available} = $available; +- next; ++sub get_links_ion { ++ my $self = shift; ++ my $prog = shift; ++ my $prog_type = shift; ++ # Hack to get correct 'channels' method because this methods is being shared with Programme::radio ++ my %channels = %{ main::progclass($prog_type)->channels_filtered( main::progclass($prog_type)->channels() ) }; ++ my $bbc_prog_page_prefix = 'http://www.bbc.co.uk/programmes'; # /$pid ++ # Setup User agent ++ my $ua = main::create_ua( 'desktop', 1 ); ++ # Download index feed ++ my @channel_list; ++ @channel_list = sort keys %channels; ++ for my $channel_id ( @channel_list ) { ++ my @ranges; ++ push @ranges, ('a-z', '0-9'); ++ if ( $prog_type eq 'tv' ) { ++ push @ranges, ('signed_a-z', 'signed_0-9'); ++ } ++ for my $range ( @ranges ) { ++ my $cond = $range; ++ my $op = "letters"; ++ my $cat; ++ if ( $cond =~ /signed_/ ) { ++ $cond =~ s/signed_//; ++ $cat = "category/signed"; ++ } ++ my $url = "http://www.bbc.co.uk/iplayer/ion/atoz/format/xml/service_type/${prog_type}/masterbrand/$channel_id/$op/$cond/$cat"; ++ main::logger "\nDEBUG: Getting feed $url\n" if $opt->{verbose}; ++ my $xml = main::request_url_retry($ua, $url, 3, '.', "\nWARNING: Failed to get programme index feed for $channels{$channel_id} - $cond $cat\n"); ++ decode_entities($xml); ++ # Parse XML ++ # get list of entries within <entry> </entry> tags ++ my @entries = split /<episode>/, $xml; ++ # Discard first element == header ++ shift @entries; ++ main::logger "\nINFO: Got ".($#entries + 1)." programmes for $channels{$channel_id} - $cond $cat\n" if $opt->{verbose}; ++ foreach my $entry (@entries) { ++ my ( $title, $name, $episode, $episodetitle, $nametitle, $episodenum, $seriesnum, $desc, $pid, $available, $channel, $duration, $thumbnail, $version, $guidance ); ++ $pid = $1 if $entry =~ m{</passionsite_title>.*?<id>(.+?)</id>}s; ++ $duration = $1 if $entry =~ m{<duration>(.*?)</duration>}; ++ $title = $1 if $entry =~ m{<complete_title>(.*?)</complete_title>}; ++ # determine name and episode from title ++ ( $name, $episode ) = Programme::bbciplayer::split_title( $title ); ++ $episodetitle = $1 if $entry =~ m{<short_synopsis>.*?<title>(.*?)</title>}s; ++ $nametitle = $name; ++ # Extract the seriesnum ++ my $regex = 'Series\s+'.main::regex_numbers(); ++ $seriesnum = main::convert_words_to_number( $1 ) if "$name $episode" =~ m{$regex}i; ++ # Extract the episode num ++ my $regex_1 = 'Episode\s+'.main::regex_numbers(); ++ my $regex_2 = '^'.main::regex_numbers().'\.\s+'; ++ if ( "$name $episode" =~ m{$regex_1}i ) { ++ $episodenum = main::convert_words_to_number( $1 ); ++ } elsif ( $episode =~ m{$regex_2}i ) { ++ $episodenum = main::convert_words_to_number( $1 ); ++ } elsif ( $episodetitle =~ m{$regex_2}i ) { ++ $episodenum = main::convert_words_to_number( $1 ); ++ } ++ # Re-insert the episode number if the episode text doesn't have it ++ if ( $episodenum && $episodetitle =~ /^\d+\./ && $episode !~ /^(.+:\s+)?\d+\./ ) { ++ $episode =~ s/^(.+:\s+)?(.*)$/$1$episodenum. $2/; ++ } ++ $desc = $1 if $entry =~ m{<short_synopsis>(.*?)</short_synopsis>}; ++ my @category; ++ my @lines = split /<category>/, $entry; ++ shift @lines; ++ for my $line ( @lines ) { ++ push @category, $1 if $line =~ m{<title>(.*?)</title>}; ++ } ++ # strip commas - they confuse sorting and spliting later ++ s/,//g for @category; ++ # Extract channel ++ $channel = $1 if $entry =~ m{<masterbrand_title>(.*?)</masterbrand_title>}; ++ main::logger "DEBUG: '$pid, $name - $episode, $channel'\n" if $opt->{debug}; ++ # Merge and Skip if this pid is a duplicate ++ if ( defined $prog->{$pid} ) { ++ main::logger "WARNING: '$pid, $prog->{$pid}->{name} - $prog->{$pid}->{episode}, $prog->{$pid}->{channel}' already exists (this channel = $channel)\n" if $opt->{verbose}; ++ # Since we use the 'Signed' (or 'Audio Described') channel to get sign zone/audio described data, merge the categories from this entry to the existing entry ++ if ( $prog->{$pid}->{categories} ne join(',', sort @category) ) { ++ my %cats; ++ $cats{$_} = 1 for ( @category, split /,/, $prog->{$pid}->{categories} ); ++ main::logger "INFO: Merged categories for $pid from $prog->{$pid}->{categories} to ".join(',', sort keys %cats)."\n" if $opt->{verbose}; ++ $prog->{$pid}->{categories} = join(',', sort keys %cats); + } +- ++ # If this is a duplicate pid and the channel is now Signed then both versions are available ++ $version = 'signed' if grep /Sign Zone/, @category; ++ $version = 'audiodescribed' if grep /Audio Described/, @category; ++ # Add version to versions for existing prog ++ $prog->{$pid}->{versions} = join ',', main::make_array_unique_ordered( (split /,/, $prog->{$pid}->{versions}), $version ); ++ next; ++ } ++ $guidance = $1 if $entry =~ m{<has_guidance>(.*?)</has_guidance>}; ++ if ( $guidance ) { ++ $guidance = "Yes"; ++ } else { ++ $guidance = undef; ++ } ++ # Check for signed-only or audiodescribed-only version from category ++ if ( grep /Sign Zone/, @category ) { ++ $version = 'signed'; ++ } elsif ( grep /Audio Described/, @category ) { ++ $version = 'audiodescribed'; ++ } else { + $version = 'default'; +- +- # Default to 150px width thumbnail; +- my $thumbsize = $opt->{thumbsizecache} || 150; +- my $thumbnail = $1 if $entry =~ m{<media:thumbnail.*?url="(.*?)"}; +- if ( $thumbnail ) { +- my $suffix = Programme::bbciplayer->thumb_url_suffixes->{ $thumbsize }; +- if ( $thumbnail !~ /$suffix/ ) { +- $thumbnail =~ s/_\d+_\d+\.jpg/$suffix/; +- } +- } +- +- # Don't create this prog instance if the availablity is in the past +- # this prevents programmes which never appear in iPlayer from being indexed +- next if Programme::get_time_string( $available ) < $now; +- +- # build data structure +- $prog->{$pid} = main::progclass($prog_type)->new( +- 'pid' => $pid, +- 'name' => $name, +- 'versions' => $version, +- 'episode' => $episode, +- 'seriesnum' => $seriesnum, +- 'episodenum' => $episodenum, +- 'desc' => $desc, +- 'available' => $available, +- 'duration' => $duration, +- 'thumbnail' => $thumbnail, +- 'channel' => $channel, +- 'type' => $prog_type, +- 'web' => "${bbc_prog_page_prefix}/${pid}.html", +- ); + } +- } ++ # Default to 150px width thumbnail; ++ my $thumbsize = $opt->{thumbsizecache} || 150; ++ my $image_template_url = $1 if $entry =~ m{<image_template_url>(.*?)</image_template_url>}; ++ my $recipe = Programme::bbciplayer->thumb_url_recipes->{ $thumbsize }; ++ my $thumbnail = $image_template_url; ++ $thumbnail =~ s/\$recipe/$recipe/; ++ # build data structure ++ $prog->{$pid} = main::progclass($prog_type)->new( ++ 'pid' => $pid, ++ 'name' => $name, ++ 'versions' => $version, ++ 'episode' => $episode, ++ 'seriesnum' => $seriesnum, ++ 'episodenum' => $episodenum, ++ 'desc' => $desc, ++ 'guidance' => $guidance, ++ 'available' => 'Unknown', ++ 'duration' => $duration || 'Unknown', ++ 'thumbnail' => $thumbnail, ++ 'channel' => $channel, ++ 'categories' => join(',', sort @category), ++ 'type' => $prog_type, ++ 'web' => "${bbc_prog_page_prefix}/${pid}", ++ ); ++ } ++ } ++ } ++} + ++ ++# Usage: Programme::tv->get_links( \%prog, 'tv' ); ++# Uses: %{ channels() }, \%prog ++sub get_links { ++ my $self = shift; # ignore obj ref ++ my $prog = shift; ++ my $prog_type = shift; ++ my $feeds = lc($opt->{"refreshfeeds".${prog_type}} || $opt->{"refreshfeeds"}); ++ main::logger "INFO: Getting $prog_type Index Feeds (this may take a few minutes)\n"; ++ if ( $feeds eq 'schedule' ) { ++ return $self->get_links_schedule($prog, $prog_type, 0); ++ } else { ++ if ( $prog_type eq 'radio' ) { ++ return $self->get_links_aod($prog, $prog_type); ++ } ++ elsif ( $prog_type eq 'tv' ) { ++ return $self->get_links_ion($prog, $prog_type); + } + } ++ ++ if ( $opt->{refreshfuture} ) { ++ $self->get_links_schedule($prog, $prog_type, 1); ++ } ++ + main::logger "\n"; + return 0; + } + + ++# get cache info for programmes from schedule ++sub get_links_schedule { ++ my $self = shift; ++ my $prog = shift; ++ my $prog_type = shift; ++ my $future = shift; ++ my %channels = %{ main::progclass($prog_type)->channels_filtered( main::progclass($prog_type)->channels_schedule() ) }; ++ my @channel_list = sort keys %channels; ++ my @schedule_dates; ++ my $limit = 0; ++ my $limit_days = $opt->{"refreshlimit".${prog_type}} || $opt->{"refreshlimit"}; ++ $limit_days = 30 if $limit_days > 30; ++ if ( $limit_days ) { ++ my $now = time(); ++ $limit = $now - $limit_days * 86400; ++ my ($limit_weeks, $rem) = (int $limit_days / 7, $limit_days % 7); ++ $limit_weeks++ if $rem && $limit_weeks; ++ for (my $i = $limit_weeks; $i >= 0; $i -= 1) { ++ my $then = $now - ($i * 7) * 86400; ++ my $year = (gmtime($then))[5]; ++ my $week = strftime( "%W", gmtime($then) ); ++ push @schedule_dates, sprintf("%04d/w%02d", $year+1900, ++$week); ++ } ++ } else { ++ if ( $future ) { ++ @schedule_dates = ( "this_week", "next_week" ); ++ } else { ++ @schedule_dates = ( "last_week", "this_week" ); ++ } ++ } ++ for my $channel_id ( @channel_list ) { ++ for my $schedule_date ( @schedule_dates ) { ++ my $url = "http://www.bbc.co.uk/${channel_id}/${schedule_date}.xml"; ++ my $rc = $self->get_links_schedule_page($prog, $prog_type, $channels{$channel_id}, $future, $url, $limit); ++ if ( $rc ) { ++ main::logger("\nWARNING: Failed to get programme schedule feed for $channel_id from iplayer site\n"); ++ } ++ } ++ } ++} ++ ++# get cache info from schedule page ++sub get_links_schedule_page { ++ my $self = shift; ++ my $prog = shift; ++ my $prog_type = shift; ++ my $channel = shift; ++ my $future = shift; ++ my $url = shift; ++ my $limit = shift; ++ my $bbc_prog_page_prefix = 'http://www.bbc.co.uk/programmes'; # /$pid ++ my $ua = main::create_ua( 'desktop', 1 ); ++ main::logger "DEBUG: Getting feed $url\n" if $opt->{debug}; ++ my $xml = main::request_url_retry($ua, $url, 3, '.', "\nWARNING: Failed to download programme schedule $url\n"); ++ return 1 if ! $xml; ++ decode_entities($xml); ++ ++ # <broadcast is_repeat="0" is_blanked="0"> ++ # <pid>p0290kxs</pid> ++ # <start>2014-10-31T11:00:00Z</start> ++ # <end>2014-10-31T11:45:00Z</end> ++ # <duration>2700</duration> ++ # <programme type="episode"> ++ # <pid>b04n8rx0</pid> ++ # <position>10</position> ++ # <title>Episode 10</title> ++ # <short_synopsis>Council officers deal with home owners living on top of deadly waste.</short_synopsis> ++ # <media_type>audio_video</media_type> ++ # <duration>2700</duration> ++ # <image> ++ # <pid>p028r5jx</pid> ++ # </image> ++ # <display_titles> ++ # <title>Call the Council</title> ++ # <subtitle>Series 2, Episode 10</subtitle> ++ # </display_titles> ++ # <first_broadcast_date>2014-10-31T11:00:00Z</first_broadcast_date> ++ # <ownership> ++ # <service type="tv" id="bbc_one" key="bbcone"> ++ # <title>BBC One</title> ++ # </service> ++ # </ownership> ++ # <programme type="series"> ++ # <pid>b04mlq1k</pid> ++ # <title>Series 2</title> ++ # <position>2</position> ++ # <image> ++ # <pid>p028r5jx</pid> ++ # </image> ++ # <expected_child_count>10</expected_child_count> ++ # <first_broadcast_date>2014-10-20T11:00:00+01:00</first_broadcast_date> ++ # <ownership> ++ # <service type="tv" id="bbc_one" key="bbcone"> ++ # <title>BBC One</title> ++ # </service> ++ # </ownership> ++ # <programme type="brand"> ++ # <pid>b04mlpdd</pid> ++ # <title>Call the Council</title> ++ # <position/> ++ # <image> ++ # <pid>p028r5jx</pid> ++ # </image> ++ # <expected_child_count/> ++ # <first_broadcast_date>2014-05-19T11:30:00+01:00</first_broadcast_date> ++ # <ownership> ++ # <service type="tv" id="bbc_one" key="bbcone"> ++ # <title>BBC One</title> ++ # </service> ++ # </ownership> ++ # </programme> ++ # </programme> ++ # <available_until>2014-12-03T07:45:00Z</available_until> ++ # <actual_start>2014-10-31T11:45:00Z</actual_start> ++ # <is_available_mediaset_pc_sd>1</is_available_mediaset_pc_sd> ++ # <is_legacy_media>0</is_legacy_media> ++ # <media format="video"> ++ # <expires>2014-12-03T07:45:00Z</expires> ++ # <availability>1 month left to watch</availability> ++ # </media> ++ # </programme> ++ # </broadcast> ++ ++ # get list of entries within <broadcast> </broadcast> tags ++ my @entries = split /<broadcast[^s]/, $xml; ++ # Discard first element == header ++ shift @entries; ++ main::logger "\nINFO: Got ".($#entries + 1)." programmes for $channel\n" if $opt->{verbose}; ++ my $now = time(); ++ foreach my $entry (@entries) { ++ my ( $title, $name, $episode, $episodetitle, $nametitle, $seriestitle, $episodenum, $seriesnum, $desc, $pid, $available, $duration, $thumbnail, $version, $guidance, $descshort, $start ); ++ $start = $1 if $entry =~ m{<start>\s*(.+?)\s*</start>}; ++ next if ! $start; ++ my $xstart = Programme::get_time_string( $start ); ++ next if $future && $xstart < $now; ++ next if ! $future && $xstart >= $now; ++ next if ! $future && $limit && $xstart < $limit; ++ $available = $1 if $entry =~ m{<available_until>\s*(.+?)\s*</available_until>}; ++ my $availability = $1 if $entry =~ m{<availability>.*?(Available).*?</availability>}i; ++ next if ! ( $available || $availability ); ++ if ( $available ) { ++ my $xavailable = Programme::get_time_string( $available ); ++ next if $xavailable < $now; ++ } ++ $pid = $1 if $entry =~ m{<programme\s+type="episode">.*?<pid>\s*(.+?)\s*</pid>}; ++ $episode = $1 if $entry =~ m{<programme\s+type="episode">.*?<title>\s*(.*?)\s*</title>}; ++ $nametitle = $1 if $entry =~ m{<programme\s+type="brand">.*?<title>\s*(.*?)\s*</title>.*?</programme>}; ++ $seriestitle = $1 if $entry =~ m{<programme\s+type="series">.*?<title>\s*(.*?)\s*</title>.*?</programme>}; ++ # Set name ++ if ( $nametitle && $seriestitle ) { ++ $name = "$nametitle: $seriestitle"; ++ } elsif ( $seriestitle && ! $nametitle ) { ++ $name = $seriestitle; ++ # Fallback to episode name if the BBC missed out both Series and Name ++ } elsif ( ( ! $seriestitle ) && ! $nametitle ) { ++ $name = $episode; ++ } else { ++ $name = $nametitle; ++ } ++ # Extract the seriesnum ++ my $regex = 'Series\s+'.main::regex_numbers(); ++ $seriesnum = main::convert_words_to_number( $1 ) if $seriestitle =~ m{$regex}i; ++ my $series_position = $1 if $entry =~ m{<programme\s+type="series">.*?<position>\s*(.+?)\s*</position>}; ++ $seriesnum ||= $series_position; ++ # Extract the episode num ++ my $regex_1 = 'Episode\s+'.main::regex_numbers(); ++ my $regex_2 = '^'.main::regex_numbers().'\.\s+'; ++ if ( $episode =~ m{$regex_1}i ) { ++ $episodenum = main::convert_words_to_number( $1 ); ++ } elsif ( $episode =~ m{$regex_2}i ) { ++ $episodenum = main::convert_words_to_number( $1 ); ++ } ++ my $episode_position = $1 if $entry =~ m{<programme\s+type="episode">.*?<position>\s*(.+?)\s*</position>}; ++ $episodenum ||= $episode_position; ++ # extract desc ++ if ( $entry =~ m{<long_synopsis>\s*(.+?)\s*</long_synopsis>} ) { ++ $desc = $1; ++ } elsif ( $entry =~ m{<medium_synopsis>\s*(.+?)\s*</medium_synopsis>} ) { ++ $desc = $1; ++ } elsif ( $entry =~ m{<short_synopsis>\s*(.+?)\s*</short_synopsis>} ) { ++ $desc = $1; ++ }; ++ # Remove unwanted html tags ++ $desc =~ s!</?(br|b|i|p|strong)\s*/?>!!gi; ++ $duration = $1 if $entry =~ m{<duration>\s*(.+?)\s*</duration>}; ++ # Extract channel nice name ++ # $channel = $channels{$channel_id}; ++ main::logger "DEBUG: '$pid, $name - $episode, $channel'\n" if $opt->{debug}; ++ # Merge and Skip if this pid is a duplicate ++ if ( defined $prog->{$pid} ) { ++ main::logger "WARNING: '$pid, $prog->{$pid}->{name} - $prog->{$pid}->{episode}, $prog->{$pid}->{channel}' already exists (this channel = $channel)\n" if $opt->{verbose}; ++ # Update this info from schedule (not available in the usual iplayer channels feeds) ++ $prog->{$pid}->{duration} = $duration; ++ $prog->{$pid}->{episodenum} = $episodenum if ! $prog->{$pid}->{episodenum}; ++ $prog->{$pid}->{seriesnum} = $seriesnum if ! $prog->{$pid}->{seriesnum}; ++ # don't add this as some progs are already available ++ #$prog->{$pid}->{available} = $available; ++ next; ++ } ++ $version = 'default'; ++ # thumbnail options ++ # http://ichef.bbci.co.uk/programmeimages/p01m1x5p/b04l8sml_640_360.jpg ++ # http://ichef.bbci.co.uk/images/ic/640x360/p01m1x5p.jpg ++ # Default to 150px width thumbnail; ++ my $thumbsize = $opt->{thumbsizecache} || 150; ++ my $image_pid = $1 if $entry =~ m{<image><pid>(.*?)</pid>}s; ++ my $suffix = Programme::bbciplayer->thumb_url_suffixes->{ $thumbsize }; ++ my $thumbnail = "http://ichef.bbci.co.uk/programmeimages/${image_pid}/${pid}${suffix}"; ++ # build data structure ++ $prog->{$pid} = main::progclass($prog_type)->new( ++ 'pid' => $pid, ++ 'name' => $name, ++ 'versions' => $version, ++ 'episode' => $episode, ++ 'seriesnum' => $seriesnum, ++ 'episodenum' => $episodenum, ++ 'desc' => $desc, ++ 'available' => $start, ++ 'duration' => $duration, ++ 'thumbnail' => $thumbnail, ++ 'channel' => $channel, ++ 'type' => $prog_type, ++ 'web' => "${bbc_prog_page_prefix}/${pid}", ++ ); ++ } ++} ++ + + # Usage: download (<prog>, <ua>, <mode>, <version>, <version_pid>) + sub download { +@@ -7886,6 +7940,72 @@ + # Class vars + sub index_min { return 10001 } + sub index_max { return 19999 }; ++sub channels_aod { ++ return { ++ # national stations ++ '1xtra' => 'BBC Radio 1Xtra', ++ 'radio1' => 'BBC Radio 1', ++ 'radio2' => 'BBC Radio 2', ++ 'radio3' => 'BBC Radio 3', ++ 'radio4' => 'BBC Radio 4', ++ 'radio4extra' => 'BBC Radio 4 Extra', ++ 'fivelive' => 'BBC Radio 5 live', ++ 'sportsextra' => 'BBC 5 live sports extra', ++ '6music' => 'BBC 6 Music', ++ 'asiannetwork' => 'BBC Asian Network', ++ # nations ++ 'radiofoyle' => 'BBC Radio Foyle', ++ 'radioscotland' => 'BBC Radio Scotland', ++ 'alba' => 'BBC Radio Nan Gaidheal', ++ 'radioulster' => 'BBC Radio Ulster', ++ 'radiowales' => 'BBC Radio Wales', ++ 'radiocymru' => 'BBC Radio Cymru', ++ 'worldservice' => 'BBC World Service', ++ # local ++ 'bbc_radio_cumbria' => 'BBC Radio Cumbria', ++ 'bbc_radio_newcastle' => 'BBC Newcastle', ++ 'bbc_tees' => 'BBC Tees', ++ 'bbc_radio_lancashire' => 'BBC Radio Lancashire', ++ 'bbc_radio_merseyside' => 'BBC Radio Merseyside', ++ 'bbc_radio_manchester' => 'BBC Radio Manchester', ++ 'bbc_radio_leeds' => 'BBC Radio Leeds', ++ 'bbc_radio_sheffield' => 'BBC Radio Sheffield', ++ 'bbc_radio_york' => 'BBC Radio York', ++ 'bbc_radio_humberside' => 'BBC Radio Humberside', ++ 'bbc_radio_lincolnshire' => 'BBC Radio Lincolnshire', ++ 'bbc_radio_nottingham' => 'BBC Radio Nottingham', ++ 'bbc_radio_leicester' => 'BBC Radio Leicester', ++ 'bbc_radio_derby' => 'BBC Radio Derby', ++ 'bbc_radio_stoke' => 'BBC Radio Stoke', ++ 'bbc_radio_shropshire' => 'BBC Radio Shropshire', ++ 'bbc_wm' => 'BBC WM 95.6', ++ 'bbc_radio_coventry_warwickshire' => 'BBC Coventry & Warwickshire', ++ 'bbc_radio_hereford_worcester' => 'BBC Hereford & Worcester', ++ 'bbc_radio_northampton' => 'BBC Radio Northampton', ++ 'bbc_three_counties_radio' => 'BBC Three Counties Radio', ++ 'bbc_radio_cambridge' => 'BBC Radio Cambridgeshire', ++ 'bbc_radio_norfolk' => 'BBC Radio Norfolk', ++ 'bbc_radio_suffolk' => 'BBC Radio Suffolk', ++ 'bbc_radio_essex' => 'BBC Essex', ++ 'bbc_london' => 'BBC London 94.9', ++ 'bbc_radio_kent' => 'BBC Radio Kent', ++ 'bbc_radio_surrey' => 'BBC Surrey', ++ 'bbc_radio_sussex' => 'BBC Sussex', ++ 'bbc_radio_oxford' => 'BBC Radio Oxford', ++ 'bbc_radio_berkshire' => 'BBC Radio Berkshire', ++ 'bbc_radio_solent' => 'BBC Radio Solent', ++ 'bbc_radio_gloucestershire' => 'BBC Radio Gloucestershire', ++ 'bbc_radio_wiltshire' => 'BBC Wiltshire', ++ 'bbc_radio_bristol' => 'BBC Radio Bristol', ++ 'bbc_radio_somerset_sound' => 'BBC Somerset', ++ 'bbc_radio_devon' => 'BBC Radio Devon', ++ 'bbc_radio_cornwall' => 'BBC Radio Cornwall', ++ 'bbc_radio_guernsey' => 'BBC Radio Guernsey', ++ 'bbc_radio_jersey' => 'BBC Radio Jersey', ++ 'bbc_radio_jersey' => 'BBC Radio Jersey', ++ }; ++} ++ + sub channels { + return { + 'bbc_1xtra' => 'BBC Radio 1Xtra', +@@ -7897,7 +8017,7 @@ + 'bbc_radio_five_live' => 'BBC Radio 5 live', + 'bbc_radio_five_live_sports_extra' => 'BBC 5 live sports extra', + 'bbc_6music' => 'BBC 6 Music', +- 'bbc_7' => 'BBC 7', ++ #'bbc_7' => 'BBC 7', + 'bbc_asian_network' => 'BBC Asian Network', + 'bbc_radio_foyle' => 'BBC Radio Foyle', + 'bbc_radio_scotland' => 'BBC Radio Scotland', +@@ -7946,8 +8066,6 @@ + 'bbc_radio_cornwall' => 'BBC Radio Cornwall', + 'bbc_radio_guernsey' => 'BBC Radio Guernsey', + 'bbc_radio_jersey' => 'BBC Radio Jersey', +- 'popular/radio' => 'Popular', +- 'highlights/radio' => 'Highlights', + }; + } + +--- get-iplayer-2.87.orig/get_iplayer.1 ++++ get-iplayer-2.87/get_iplayer.1 +@@ -508,12 +508,30 @@ + \fB\-\-refresh\-exclude <string> + Exclude matched channel(s) when refreshing cache (regex or comma separated values) + .TP ++\fB\-\-refresh\-feeds <string> ++Alternate source for programme data. Valid values: 'schedule' ++.TP ++\fB\-\-refresh\-feeds\-radio <string> ++Alternate source for radio programme data. Valid values: 'schedule' ++.TP ++\fB\-\-refresh\-feeds\-tv <string> ++Alternate source for TV programme data. Valid values: 'schedule' ++.TP + \fB\-\-refresh\-future + Obtain future programme schedule when refreshing cache (between 7\-14 days) + .TP + \fB\-\-refresh\-include <string> + Include matched channel(s) when refreshing cache (regex or comma separated values) + .TP ++\fB\-\-refresh\-limit <integer> ++Number of days of programmes to cache. Only applied with \-\-refresh\-feeds=schedule. Makes cache updates VERY slow. Default: 7 Min: 1 Max: 30 ++.TP ++\fB\-\-refresh\-limit\-radio <integer> ++Number of days of radio programmes to cache. Only applied with \-\-refresh\-feeds=schedule. Makes cache updates VERY slow. Default: 7 Min: 1 Max: 30 ++.TP ++\fB\-\-refresh\-limit\-tv <integer> ++Number of days of TV programmes to cache. Only applied with \-\-refresh\-feeds=schedule. Makes cache updates VERY slow. Default: 7 Min: 1 Max: 30 ++.TP + \fB\-\-skipdeleted + Skip the download of metadata/thumbs/subs if the media file no longer exists. Use with \-\-history & \-\-metadataonly/subsonly/thumbonly. + .TP diff -Nru get-iplayer-2.87/debian/patches/data-feeds.patch get-iplayer-2.87/debian/patches/data-feeds.patch --- get-iplayer-2.87/debian/patches/data-feeds.patch 1970-01-01 01:00:00.000000000 +0100 +++ get-iplayer-2.87/debian/patches/data-feeds.patch 2014-12-04 23:22:09.000000000 +0000 @@ -0,0 +1,221 @@ +From: dinkypumpkin <dinkypump...@gmail.com> +Date: Fri, 31 Oct 2014 12:40:29 +0000 +Subject: Fixes for metadata problems due to BBC removing data feeds +Origin: http://git.infradead.org/get_iplayer.git/commit/3b11bf9baffd66e17d18116eb546566f84ae7927 +Last-Update: 2014-12-03 + +--- get-iplayer-2.87.orig/get_iplayer ++++ get-iplayer-2.87/get_iplayer +@@ -5421,28 +5421,26 @@ + # flatten + $xml =~ s/\n/ /g; + ++ # set title here - broken in JSON playlists ++ $prog->{title} = decode_entities($1) if $xml =~ m{<title>\s*(.+?)\s*<\/title>}; ++ $prog->{thumbnail} = $1 if $xml =~ m{<link rel="holding" href="(.*?)"}; ++ $prog->{guidance} = $1 if $xml =~ m{<guidance.*?>(.*?)</guidance>}; ++ $prog->{descshort} = $1 if $xml =~ m{<summary>(.*?)</summary>}; ++ $prog->{type} = 'tv' if grep /kind="programme"/, $xml; ++ $prog->{type} = 'radio' if grep /kind="radioProgramme"/, $xml; ++ + # Detect noItems or no programmes + if ( $xml =~ m{<noItems\s+reason="(\w+)"} || $xml !~ m{kind="(programme|radioProgramme)"} ) { +- # set title here - broken in JSON playlists +- $prog->{title} = decode_entities($1) if $xml =~ m{<title>\s*(.+?)\s*<\/title>}; +- my $rc_verpids = $prog->get_verpids_json( $ua ); +- if ( $rc_verpids ) { +- $rc_verpids = $prog->get_verpids_html( $ua ) ++ my $rc_json = $prog->get_verpids_json( $ua ); ++ my $rc_html = 1; ++ if ( ! $prog->{type} || $prog->{type} eq 'tv' ) { ++ $rc_html = $prog->get_verpids_html( $ua ); + } +- return 0 if ! $rc_verpids; +- main::logger "\rWARNING: No programmes are available for this pid with version(s): ".($opt->{versionlist} ? $opt->{versionlist} : 'default').($prog->{versions} ? " (available versions: $prog->{versions})\n" : "\n"); ++ return 0 if ! $rc_json || ! $rc_html; ++ main::logger "\nWARNING: No programmes are available for this pid with version(s): ".($opt->{versionlist} ? $opt->{versionlist} : 'default').($prog->{versions} ? " (available versions: $prog->{versions})\n" : "\n"); + return 1; + } + +- # Get title +- # <title>Amazon with Bruce Parry: Episode 1</title> +- my ( $title, $prog_type ); +- $title = $1 if $xml =~ m{<title>\s*(.+?)\s*<\/title>}; +- +- # Get type +- $prog_type = 'tv' if grep /kind="programme"/, $xml; +- $prog_type = 'radio' if grep /kind="radioProgramme"/, $xml; +- + # Split into <item kind="programme"> sections + my $prev_version = ''; + for ( split /<item\s+kind="(radioProgramme|programme)"/, $xml ) { +@@ -5530,7 +5528,6 @@ + + # Add to prog hash + $prog->{versions} = join ',', keys %{ $prog->{verpids} }; +- $prog->{title} = decode_entities($title); + return 0; + } + +@@ -5556,30 +5553,37 @@ + return 1; + } + my ( $default, $versions ) = split /"allAvailableVersions"/, $json; +- my $channel = $1 if $default =~ /"masterBrandName":"(.*?)"/; +- $prog->{channel} = $channel if $channel; +- my $descshort = $1 if $default =~ /"summary":"(.*?)"/; +- $prog->{descshort} = $descshort if $descshort; +- my $guidance = $2 if $default =~ /"guidance":(null|"(.*?)")/; +- $prog->{guidance} = "Yes" if $guidance; +- my $thumbnail = $1 if $default =~ /"holdingImageURL":"(.*?)"/; +- $thumbnail =~ s/\\\//\//g; +- my $thumbsize = $opt->{thumbsize} || $opt->{thumbsizecache} || 150; +- my $recipe = Programme::bbciplayer->thumb_url_recipes->{ $thumbsize }; +- if ( ! $recipe ) { +- main::logger "WARNING: Invalid thumbnail size: $thumbsize - using default (JSON)\n"; +- $recipe = Programme::bbciplayer->thumb_url_recipes->{ 150 }; +- } +- $thumbnail =~ s/\$recipe/$recipe/; +- $prog->{thumbnail} = $thumbnail if $thumbnail; +- if ( ! $prog->{title} ) { ++ unless ( $prog->{channel} ) { ++ $prog->{channel} = $1 if $default =~ /"masterBrandName":"(.*?)"/; ++ } ++ unless ( $prog->{descshort} ) { ++ $prog->{descshort} = $1 if $default =~ /"summary":"(.*?)"/; ++ } ++ unless ( $prog->{guidance} ) { ++ my $guidance = $2 if $default =~ /"guidance":(null|"(.*?)")/; ++ $prog->{guidance} = "Yes" if $guidance; ++ } ++ unless ( $prog->{thumbnail} ) { ++ my $thumbnail = $1 if $default =~ /"holdingImageURL":"(.*?)"/; ++ $thumbnail =~ s/\\\//\//g; ++ my $thumbsize = $opt->{thumbsize} || $opt->{thumbsizecache} || 150; ++ my $recipe = Programme::bbciplayer->thumb_url_recipes->{ $thumbsize }; ++ if ( ! $recipe ) { ++ main::logger "WARNING: Invalid thumbnail size: $thumbsize - using default (JSON)\n"; ++ $recipe = Programme::bbciplayer->thumb_url_recipes->{ 150 }; ++ } ++ $thumbnail =~ s/\$recipe/$recipe/; ++ $prog->{thumbnail} = $thumbnail if $thumbnail; ++ } ++ unless ( $prog->{title} ) { + my $title = $1 if $default =~ /"title":"(.*?)"/; + $title =~ s/\\\//\//g; + $prog->{title} = decode_entities($title) if $title; + } +- my $prog_type = 'tv' if $default =~ /"kind":"video"/; +- $prog_type = 'radio' if $default =~ /"kind":"audio"/; +- $prog->{type} = $prog_type if $prog_type; ++ unless ( $prog->{type} ) { ++ $prog->{type} = 'tv' if $default =~ /"kind":"video"/; ++ $prog->{type} = 'radio' if $default =~ /"kind":"audio"/; ++ } + my $version_json = { + 'DubbedAudioDescribed' => 'audiodescribed', + 'Signed' => 'signed' +@@ -5609,6 +5613,7 @@ + } + unless ( $prog->{player} ) { + $prog->{player} = $episode_url if $episode_url; ++ last; + } + } + } +@@ -5636,8 +5641,10 @@ + main::logger "INFO: pid changed from $prog->{pid} to $pid (HTML)\n" if $opt->{verbose}; + $prog->{pid} = $pid; + } ++ my $version_list = $opt->{versionlist} || 'default'; + my $version_map = { "default" => "", "audiodescribed" => "ad", "signed" => "sign"}; + for my $version ( "default", "audiodescribed", "signed" ) { ++ next if $version_list !~ /$version/ || $prog->{verpids}->{$version}; + my $url = 'http://www.bbc.co.uk/iplayer/episode/'.$pid."/$version_map->{$version}"; + main::logger "INFO: iPlayer metadata URL (HTML) [$version] = $url\n" if $opt->{verbose}; + my $html = main::request_url_retry( $ua, $url, 3 ); +@@ -5653,12 +5660,10 @@ + next; + } + unless ( $prog->{channel} ) { +- my $channel = $1 if $config =~ /"masterBrandTitle":"(.*?)"/; +- $prog->{channel} = $channel if $channel; ++ $prog->{channel} = $1 if $config =~ /"masterBrandTitle":"(.*?)"/; + } + unless ( $prog->{descshort} ) { +- my $descshort = $1 if $config =~ /"summary":"(.*?)"/; +- $prog->{descshort} = $descshort if $descshort; ++ $prog->{descshort} = $1 if $config =~ /"summary":"(.*?)"/; + } + unless ( $prog->{guidance} ) { + my $guidance = $2 if $config =~ /"guidance":(null|"(.*?)")/; +@@ -5691,7 +5696,6 @@ + $prog->{durations}->{$version} = $1 if $config =~ /"duration":(\d+)/; + } + $prog->{versions} = join ',', keys %{ $prog->{verpids} }; +- my $version_list = $opt->{versionlist} || 'default'; + for ( split /,/, $version_list ) { + if ( $prog->{verpids}->{$_} ) { + my $episode_url; +@@ -5702,6 +5706,7 @@ + } + unless ( $prog->{player} ) { + $prog->{player} = $episode_url if $episode_url; ++ last; + } + } + } +@@ -5806,7 +5811,8 @@ + #</feed> + + # Don't get metadata from this URL if the pid contains a full url (problem: this still tries for BBC iPlayer live channels) +- if ( $prog->{pid} !~ m{^http}i ) { ++ # skip this section since BBC killed feeds, but keep code for reference ++ if ( $prog->{pid} !~ m{^http}i && 0) { + $entry = main::request_url_retry($ua, $prog_feed_url.$prog->{pid}, 3, '', ''); + decode_entities($entry); + main::logger "DEBUG: $prog_feed_url.$prog->{pid}:\n$entry\n\n" if $opt->{debug}; +@@ -5870,6 +5876,21 @@ + } + } + ++ # Get list of available modes for each version available ++ # populate version pid metadata if we don't have it already ++ if ( keys %{ $prog->{verpids} } == 0 ) { ++ if ( $prog->get_verpids( $ua ) ) { ++ main::logger "ERROR: Could not get version pid metadata\n" if $opt->{verbose}; ++ # Only return at this stage unless we want metadata/tags only for various reasons ++ return 1 if ! ( $opt->{info} || $opt->{metadataonly} || $opt->{thumbonly} || $opt->{tagonly} ) ++ } ++ } ++ # Re-split title if changed in get_verpids() ++ #if ( $prog->{title} && $prog->{title} ne $title ) { ++ # $title = $prog->{title}; ++ # ( $name, $episode ) = Programme::bbciplayer::split_title( $title ); ++ #} ++ $versions = join ',', sort keys %{ $prog->{verpids} }; + + # Even more info... + #<?xml version="1.0" encoding="utf-8"?> +@@ -5957,21 +5978,6 @@ + $categories ||= "get_iplayer"; + $category ||= "get_iplayer"; + +- # Get list of available modes for each version available +- # populate version pid metadata if we don't have it already +- if ( keys %{ $prog->{verpids} } == 0 ) { +- if ( $prog->get_verpids( $ua ) ) { +- main::logger "ERROR: Could not get version pid metadata\n" if $opt->{verbose}; +- # Only return at this stage unless we want metadata/tags only for various reasons +- return 1 if ! ( $opt->{info} || $opt->{metadataonly} || $opt->{thumbonly} || $opt->{tagonly} ) +- } +- } +- # Re-split title if changed in get_verpids() +- if ( $prog->{title} && $prog->{title} ne $title ) { +- $title = $prog->{title}; +- ( $name, $episode ) = Programme::bbciplayer::split_title( $title ); +- } +- $versions = join ',', sort keys %{ $prog->{verpids} }; + my $modes; + my $mode_sizes; + my $first_broadcast; diff -Nru get-iplayer-2.87/debian/patches/future-schedule.patch get-iplayer-2.87/debian/patches/future-schedule.patch --- get-iplayer-2.87/debian/patches/future-schedule.patch 1970-01-01 01:00:00.000000000 +0100 +++ get-iplayer-2.87/debian/patches/future-schedule.patch 2014-12-04 23:22:09.000000000 +0000 @@ -0,0 +1,81 @@ +From: dinkypumpkin <dinkypump...@gmail.com> +Date: Sun, 2 Nov 2014 21:24:12 +0000 +Subject: Restored future schedule refresh +Origin: http://git.infradead.org/get_iplayer.git/commitdiff/cad12b98c56d796c3e73210eba5f348b08385d3a?hp=9ddffb88f05dd15c6e9192cd8df04a71d82a6a67 +Last-Update: 2014-12-03 + +--- get-iplayer-2.87.orig/get_iplayer ++++ get-iplayer-2.87/get_iplayer +@@ -7410,15 +7410,15 @@ + my $prog = shift; + my $prog_type = shift; + my $feeds = lc($opt->{"refreshfeeds".${prog_type}} || $opt->{"refreshfeeds"}); +- main::logger "INFO: Getting $prog_type Index Feeds (this may take a few minutes)\n"; ++ main::logger "\nINFO: Getting $prog_type Index Feeds (this may take a few minutes)\n"; + if ( $feeds eq 'schedule' ) { +- return $self->get_links_schedule($prog, $prog_type, 0); ++ $self->get_links_schedule($prog, $prog_type, 0); + } else { + if ( $prog_type eq 'radio' ) { +- return $self->get_links_aod($prog, $prog_type); ++ $self->get_links_aod($prog, $prog_type); + } + elsif ( $prog_type eq 'tv' ) { +- return $self->get_links_ion($prog, $prog_type); ++ $self->get_links_ion($prog, $prog_type); + } + } + +@@ -7561,20 +7561,12 @@ + main::logger "\nINFO: Got ".($#entries + 1)." programmes for $channel\n" if $opt->{verbose}; + my $now = time(); + foreach my $entry (@entries) { +- my ( $title, $name, $episode, $episodetitle, $nametitle, $seriestitle, $episodenum, $seriesnum, $desc, $pid, $available, $duration, $thumbnail, $version, $guidance, $descshort, $start ); +- $start = $1 if $entry =~ m{<start>\s*(.+?)\s*</start>}; +- next if ! $start; +- my $xstart = Programme::get_time_string( $start ); +- next if $future && $xstart < $now; +- next if ! $future && $xstart >= $now; +- next if ! $future && $limit && $xstart < $limit; +- $available = $1 if $entry =~ m{<available_until>\s*(.+?)\s*</available_until>}; +- my $availability = $1 if $entry =~ m{<availability>.*?(Available).*?</availability>}i; +- next if ! ( $available || $availability ); +- if ( $available ) { +- my $xavailable = Programme::get_time_string( $available ); +- next if $xavailable < $now; +- } ++ my ( $title, $name, $episode, $episodetitle, $nametitle, $seriestitle, $episodenum, $seriesnum, $desc, $pid, $available, $duration, $thumbnail, $version, $guidance, $descshort ); ++ # Don't create this prog instance if the availablity is in the past ++ # this prevents programmes which never appear in iPlayer from being indexed ++ $available = $1 if $entry =~ m{<start>\s*(.+?)\s*</start>}; ++ next if $future && Programme::get_time_string( $available ) < $now; ++ next if ! $future && $limit && Programme::get_time_string( $available ) < $limit; + $pid = $1 if $entry =~ m{<programme\s+type="episode">.*?<pid>\s*(.+?)\s*</pid>}; + $episode = $1 if $entry =~ m{<programme\s+type="episode">.*?<title>\s*(.*?)\s*</title>}; + $nametitle = $1 if $entry =~ m{<programme\s+type="brand">.*?<title>\s*(.*?)\s*</title>.*?</programme>}; +@@ -7639,6 +7631,7 @@ + my $image_pid = $1 if $entry =~ m{<image><pid>(.*?)</pid>}s; + my $suffix = Programme::bbciplayer->thumb_url_suffixes->{ $thumbsize }; + my $thumbnail = "http://ichef.bbci.co.uk/programmeimages/${image_pid}/${pid}${suffix}"; ++ + # build data structure + $prog->{$pid} = main::progclass($prog_type)->new( + 'pid' => $pid, +@@ -7648,7 +7641,7 @@ + 'seriesnum' => $seriesnum, + 'episodenum' => $episodenum, + 'desc' => $desc, +- 'available' => $start, ++ 'available' => $available, + 'duration' => $duration, + 'thumbnail' => $thumbnail, + 'channel' => $channel, +@@ -7939,7 +7932,7 @@ + + # Class vars + sub index_min { return 10001 } +-sub index_max { return 19999 }; ++sub index_max { return 39999 }; + sub channels_aod { + return { + # national stations diff -Nru get-iplayer-2.87/debian/patches/live-tv.patch get-iplayer-2.87/debian/patches/live-tv.patch --- get-iplayer-2.87/debian/patches/live-tv.patch 1970-01-01 01:00:00.000000000 +0100 +++ get-iplayer-2.87/debian/patches/live-tv.patch 2014-12-04 23:22:09.000000000 +0000 @@ -0,0 +1,18 @@ +From: dinkypumpkin <dinkypump...@gmail.com> +Date: Sat, 25 Oct 2014 15:48:54 +0000 (+0100) +Subject: Fixed live TV streaming +Origin: http://git.infradead.org/get_iplayer.git/commitdiff_plain/754b2007a18c75d4aca96d153c4df65606676c09?hp=b7e729ccdc7261d6270b26c054e79876decd6ecb +Last-Update: 2014-12-03 + +--- get-iplayer-2.87.orig/get_iplayer ++++ get-iplayer-2.87/get_iplayer +@@ -5454,7 +5454,8 @@ + if ( m{\s+simulcast="true"} ) { + $version = 'default'; + # <item kind="programme" live="true" liverewind="true" identifier="bbc_two_england" group="bbc_two_england" simulcast="true" availability_class="liverewind"> +- $verpid = "http://www.bbc.co.uk/emp/simulcast/".$2.".xml" if m{\s+live="true"\s+(liverewind="true"\s+)?identifier="(.+?)"}; ++ # $verpid = "http://www.bbc.co.uk/emp/simulcast/".$2.".xml" if m{\s+live="true"\s+(liverewind="true"\s+)?identifier="(.+?)"}; ++ $verpid = $2 if m{\s+live="true"\s+(liverewind="true"\s+)?identifier="(.+?)"}; + main::logger "INFO: Using Live TV: $verpid\n" if $opt->{verbose} && $verpid; + + # Live/Non-live EMP tv/radio XML URL diff -Nru get-iplayer-2.87/debian/patches/series get-iplayer-2.87/debian/patches/series --- get-iplayer-2.87/debian/patches/series 2014-10-20 20:39:26.000000000 +0100 +++ get-iplayer-2.87/debian/patches/series 2014-12-04 23:22:09.000000000 +0000 @@ -0,0 +1,4 @@ +live-tv.patch +data-feeds.patch +cache-search.patch +future-schedule.patch
signature.asc
Description: Digital signature