Skip to content

Commit

Permalink
Use yt-dlp to get a stream for Mixcloud instead of an online API service
Browse files Browse the repository at this point in the history
Patch contributed by @internoot
  • Loading branch information
danielvijge committed Apr 14, 2024
1 parent 2f28ec9 commit a16c752
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 91 deletions.
92 changes: 59 additions & 33 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
lfs: true

- name: Set version number (release build)
if: contains(github.ref, 'refs/tags/')
Expand All @@ -31,58 +31,92 @@ jobs:
echo "RELEASE_CHANNEL=dev" >> $GITHUB_ENV
echo "FOLDER=dev-builds" >> $GITHUB_ENV
- name: Set yt-dlp version
run: |
echo "YT_VERSION=`cat yt-dlp.version`" >> $GITHUB_ENV
- name: Test if release number matches x.y
if: env.RELEASE_CHANNEL == 'release'
run: |
! [[ ${{ env.VERSION }} =~ ^[0-9]+.[0-9]+$ ]] && echo "Release tag must be in the form of x.y." && exit 1 || echo "Release tag format is correct"
- name: Create install XML file
uses: cuchi/[email protected].0
uses: cuchi/[email protected].2
with:
template: install.template.xml
output_file: install.xml
strict: true

- name: Build package
- name: Download yt-dlp binaries for Linux
run: |
zip -r lms_mixcloud-${{ env.VERSION }}.zip . -x \*.zip \*.sh \*.git\* \*README\* \*sublime-\* \*.DS_Store\* \*.template.xml
wget https://github.com/yt-dlp/yt-dlp/releases/download/${{ env.YT_VERSION }}/yt-dlp -P Bin && chmod +x Bin/yt-dlp
- name: Save artifact
uses: actions/upload-artifact@v2
with:
name: lms_mixcloud-${{ env.VERSION }}.zip
path: lms_mixcloud-${{ env.VERSION }}.zip
- name: Test yt-dlp operation with Mixcloud
run: |
./Bin/yt-dlp --skip-download --dump-json https://www.mixcloud.com/johndigweed/transitions-with-john-digweed-and-nickon-faith/ 1> /dev/null
- name: Download yt-dlp binaries for Windows and MacOS
#if: env.RELEASE_CHANNEL == 'release'
run: |
wget https://github.com/yt-dlp/yt-dlp/releases/download/${{ env.YT_VERSION }}/yt-dlp.exe -P Bin
wget https://github.com/yt-dlp/yt-dlp/releases/download/${{ env.YT_VERSION }}/yt-dlp_macos -P Bin && chmod +x Bin/yt-dlp_macos
- name: Build package for Linux
run: |
zip -r lms_mixcloud-${{ env.VERSION }}-linux.zip . -x \*.zip \*.sh \*.git\* \*README\* \*sublime-\* \*.DS_Store\* \*.template.xml Bin\.gitkeep Bin/yt-dlp\.exe Bin/yt-dlp_macos
- name: Build packages for Windows and MacOS
#if: env.RELEASE_CHANNEL == 'release'
run: |
zip -r lms_mixcloud-${{ env.VERSION }}-windows.zip . -x \*.zip \*.sh \*.git\* \*README\* \*sublime-\* \*.DS_Store\* \*.template.xml Bin\.gitkeep Bin/yt-dlp Bin/yt-dlp_macos
rm Bin/yt-dlp && mv Bin/yt-dlp_macos Bin/yt-dlp
zip -r lms_mixcloud-${{ env.VERSION }}-macos.zip . -x \*.zip \*.sh \*.git\* \*README\* \*sublime-\* \*.DS_Store\* \*.template.xml Bin\.gitkeep Bin/yt-dlp.exe
- name: Calculate SHA
- name: Calculate SHA for Linux
run: |
echo "SHA_LINUX=$(shasum lms_mixcloud-${{ env.VERSION }}-linux.zip | awk '{print $1;}')" >> $GITHUB_ENV
- name: Calculate SHA for Windows and MacOS
#if: env.RELEASE_CHANNEL == 'release'
run: |
echo "SHA=$(shasum lms_mixcloud-${{ env.VERSION }}.zip | awk '{print $1;}')" >> $GITHUB_ENV
echo "SHA_WINDOWS=$(shasum lms_mixcloud-${{ env.VERSION }}-windows.zip | awk '{print $1;}')" >> $GITHUB_ENV
echo "SHA_MACOS=$(shasum lms_mixcloud-${{ env.VERSION }}-macos.zip | awk '{print $1;}')" >> $GITHUB_ENV
- name: Create dev channel public XML file
uses: cuchi/[email protected].0
uses: cuchi/[email protected].2
with:
template: public.template.xml
output_file: public-dev.xml
strict: true
variables: |
all_platforms=true
- name: Create release public XML file
if: env.RELEASE_CHANNEL == 'release'
uses: cuchi/[email protected].0
uses: cuchi/[email protected].2
with:
template: public.template.xml
output_file: public.xml
strict: true
variables: |
all_platforms=true
- name: Switch branch, setup git for push
run: |
mv public*.xml /tmp
mv lms_mixcloud-${{ env.VERSION }}.zip /tmp
mv lms_mixcloud-${{ env.VERSION }}*.zip /tmp
git checkout gh-pages
cp /tmp/lms_mixcloud-${{ env.VERSION }}.zip ${{ env.FOLDER }}/
cp /tmp/public*.xml .
cp /tmp/lms_mixcloud-${{ env.VERSION }}*.zip ${{ env.FOLDER }}/
git config --local user.email "[email protected]"
git config --local user.name "GitHub Actions"
git add ${{ env.FOLDER }}/lms_mixcloud-${{ env.VERSION }}.zip
git commit -m "Github Actions release ${{ env.VERSION }}" -a
if [ "${{ env.RELEASE_CHANNEL }}" == "dev" ]; then
git add ${{ env.FOLDER }}/lms_mixcloud-${{ env.VERSION }}*.zip
COMMIT_MESSAGE_TYPE="development build"
else
COMMIT_MESSAGE_TYPE="release"
fi
git commit -m "Github Actions ${COMMIT_MESSAGE_TYPE} ${{ env.VERSION }}" -a
- name: Push changes to gh-pages
uses: ad-m/github-push-action@master
Expand All @@ -93,23 +127,15 @@ jobs:
- name: Create release
if: env.RELEASE_CHANNEL == 'release'
id: create_release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ env.VERSION }}
name: Release ${{ env.VERSION }}
draft: false
prerelease: false

- name: Upload Release Asset
if: env.RELEASE_CHANNEL == 'release'
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: lms_mixcloud-${{ env.VERSION }}.zip
asset_name: lms_mixcloud-${{ env.VERSION }}.zip
asset_content_type: application/zip
files: |
lms_mixcloud-${{ env.VERSION }}-linux.zip
lms_mixcloud-${{ env.VERSION }}-windows.zip
lms_mixcloud-${{ env.VERSION }}-macos.zip
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.zip
install.xml
Bin/*
Empty file added Bin/.gitkeep
Empty file.
7 changes: 7 additions & 0 deletions HTML/EN/plugins/MixCloud/settings/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@
[% END %]
[% END %]

[% WRAPPER setting title="PLUGIN_MIXCLOUD_HELPER_APPLICATION" desc="PLUGIN_MIXCLOUD_HELPER_APPLICATION_DESC" %]
<input type="radio" id="bundled" checked /><label for="bundled">[% 'PLUGIN_MIXCLOUD_HELPER_APPLICATION_BUNDLED' | getstring %]</label><br/>
<input type="radio" id="system" disabled /><label for="system">[% 'PLUGIN_MIXCLOUD_HELPER_APPLICATION_SYSTEM' | getstring %]</label><br/>
<input type="radio" id="custom" disabled /><label for="custom">[% 'PLUGIN_MIXCLOUD_HELPER_APPLICATION_CUSTOM' | getstring %]</label><br/>
<input type="text" id="custom_path" disabled />
[% END %]

[% PROCESS settings/footer.html %]
118 changes: 70 additions & 48 deletions ProtocolHandler.pm
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ use constant PAGE_URL_REGEXP => qr{^https?://(?:www|m)\.mixcloud\.com/};
use constant USER_AGENT => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:56.0; SlimServer) Gecko/20100101 Firefox/56.0';
use constant META_CACHE_TTL => 86400 * 30; # 24 hours x 30 = 30 days

use constant EXEC => 'yt-dlp';
use constant EXEC_OPTIONS => '--skip-download --dump-json';

my $log = logger('plugin.mixcloud');
my $prefs = preferences('plugin.mixcloud');
my $cache = Slim::Utils::Cache->new;

my $bin_path;

Slim::Player::ProtocolHandlers->registerURLHandler(PAGE_URL_REGEXP, __PACKAGE__);

sub isPlaylistURL { 0 }
Expand Down Expand Up @@ -143,20 +148,20 @@ sub getNextTrack {
$http->send_request( {
request => HTTP::Request->new( GET => $meta->{'url'}, [ 'User-Agent' => USER_AGENT ] ),
onStream => $meta->{format} eq 'mp3'
? \&Slim::Utils::Scanner::Remote::parseAudioStream
? \&Slim::Utils::Scanner::Remote::parseAudioStream
: \&Slim::Utils::Scanner::Remote::parseMp4Header,
onError => sub {
my ($self, $error) = @_;
$log->error( "could not find $meta->{'url'} header with format $meta->{'format'} $error" );
$successCb->();
},
passthrough => [ $song->track,
{ cb => sub {
{ cb => sub {
# See comments regarding bitrate and type in makeCacheItem.
# This line causes the actual bitrate of the stream to be cached.
$meta->{bitrate} = int($song->track->bitrate/1000) . 'kbps';
$cache->set('mixcloud_item_extra' . getId($url), $meta, META_CACHE_TTL);
$successCb->();
$successCb->();
}},
$meta->{'url'} ],
} );
Expand All @@ -167,14 +172,27 @@ sub getNextTrack {
);
}

sub findExec {
my %paths = Slim::Utils::Misc::getBinPaths();

for my $path (%paths) {
if (index($path, 'MixCloud') != -1) {
$log->debug("Use bin path " . $path);
$bin_path = $path;
return;
}
}
$log->error("Error: Cannot find bin path for yt-dlp");
}

# complement track details (url, format, bitrate) using dmixcloud
sub _fetchTrackExtra {
my ($url, $cb) = @_;
my $id = getId($url);
my $simpleMeta = $cache->get("mixcloud_item_$id") || {};
my $meta = $cache->get("mixcloud_item_extra_$id") || {};

$log->debug("Getting complement for $url => $id");
$log->debug("Getting complement for $url => $id");

# we already have everything
if ($cache->{'url'} && $simpleMeta->{'updated_time'} eq $meta->{'updated_time'}) {
Expand All @@ -184,46 +202,50 @@ sub _fetchTrackExtra {
}

my $mixcloud_url = "https://www.mixcloud.com/$id";
my $http = Slim::Networking::Async::HTTP->new;

$log->info("Fetching complement with downloader $url $mixcloud_url");

$http->send_request( {
request => HTTP::Request->new( POST => 'https://www.savelink.info/input',
[ 'User-Agent' => USER_AGENT, 'X-Requested-With' => 'XMLHttpRequest', 'Content-Type' => 'application/x-www-form-urlencoded' ],
"url=$mixcloud_url" ),
Timeout => 30,
onBody => sub {
my $content = shift->response->content;
my $json = eval { from_json($content) };

if ($json && $json->{'link'}) {
my $format = ($json->{'link'} =~ /.mp3/ ? "mp3" : "mp4");
# need to re-read from cache in case TrackDetails have been updated
$meta = $cache->get("mixcloud_item_$id") || {};
# See comments regarding bitrate and type in makeCacheItem.
# $meta->{'bitrate'} = $format eq 'mp3' ? '128k' : '64k';
$meta->{'format'} = $format;
$meta->{'type'} = "$format";
$meta->{'url'} = $json->{'link'};
$cache->set("mixcloud_item_extra_$id", $meta, META_CACHE_TTL);
$meta->{'album'} = 'Mixcloud';

$log->info("Got play URL $meta->{'url'} for $url from download");
} else {
$log->error("Empty response for play URL for $url", dump($json));
}

$cb->($meta) if $cb;
},
onError => sub {
my ($self, $error) = @_;
$log->error("Error getting play URL for $url => $error");
$cb->() if $cb;
},

if ($bin_path eq "") {
findExec();
}
# use yt-dlp to extract stream URL
my $exec = $bin_path . '/' . EXEC;
if ($^O eq 'MSWin32') {
$exec = "$exec.exe";
}
my $exec_options = EXEC_OPTIONS;
my $yt_dlp_cmd = "$exec $exec_options $mixcloud_url";
$log->info("Executing helper binary: $yt_dlp_cmd");
my $info_json_str = `$yt_dlp_cmd`;
my $json = eval { from_json($info_json_str) };

if ($json) {
my $mixcloud_stream_url;
my $mixcloud_stream_formats = $json->{'formats'};

# we're interested in the format labelled 'http' and nothing else
foreach my $mixcloud_format (@$mixcloud_stream_formats) {
if ($mixcloud_format->{'format_id'} eq 'http'){
$mixcloud_stream_url = $mixcloud_format->{'url'};
}
}
);


my $format = ($mixcloud_stream_url =~ /.mp3/ ? "mp3" : "mp4");
# need to re-read from cache in case TrackDetails have been updated
$meta = $cache->get("mixcloud_item_$id") || {};
# See comments regarding bitrate and type in makeCacheItem.
# $meta->{'bitrate'} = $format eq 'mp3' ? '128k' : '64k';
$meta->{'format'} = $format;
$meta->{'type'} = "$format";
$meta->{'url'} = $mixcloud_stream_url;
$cache->set("mixcloud_item_extra_$id", $meta, META_CACHE_TTL);
$meta->{'album'} = 'Mixcloud';

$log->info("Got play URL $meta->{'url'} for $url from download");
} else {
$log->error("Failed to determine stream URL for $url");
}

$cb->($meta) if $cb;

return $meta;
}

Expand All @@ -236,7 +258,7 @@ sub getMetadataFor {
# this is ugly... for whatever reason the EN/Classic skins can't handle tracks with an items element
if ($args ne 'forceCurrent' && ($args->{params} && $args->{params}->{isWeb} && preferences('server')->get('skin')=~ /Classic|EN/i)) {
delete @$item{'items'};
}
}

return $item if $item && $item->{'play'};

Expand Down Expand Up @@ -431,8 +453,8 @@ sub makeCacheItem {
# this is ugly... for whatever reason the EN/Classic skins can't handle tracks with an items element
my $simpleTracks = (($args->{params} && $args->{params}->{isWeb} && preferences('server')->get('skin')=~ /Classic|EN/i) ? 1 : 0);
if (!$simpleTracks) {
$item->{'items'} = $trackInfo;
}
$item->{'items'} = $trackInfo;
}

# Replace some fields if the call comes from Plugin.pm but do not cache.
if ($args->{params} && $args->{params}->{isPlugin}) {
Expand All @@ -445,8 +467,8 @@ sub makeCacheItem {
$item->{line1} = $json->{'name'} . ($duration ? ' (' . $duration . ')': '') .
($json->{'is_exclusive'} eq 1 ? (' (' . string('PLUGIN_MIXCLOUD_EXCLUSIVE_SHORT') . ')') : ''),
$item->{line2} = $json->{'user'}->{'name'} . ($year ? ' (' . $year . ')' : ''),
}
}
return $item;
}

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Mixcloud Plugin for Logitech Squeezebox media server #
# Mixcloud Plugin for Lyrion Music Server #

This is a Logitech Media Server (LMS) (a.k.a Squeezebox server) plugin to play
tracks from Mixcloud. To install, use the settings page of Logitech Media server.
This is a Lyrion Music Server (LMS) (a.k.a Squeezebox server) plugin to play
tracks from Mixcloud. To install, use the settings page of Lyrion Music server.
Go to the _Plugins_ tab, scroll down to _Third party source_ and select _Mixcloud_.
Press the _Apply_ button and restart LMS.

Expand All @@ -18,8 +18,8 @@ For the development version (updated with every commit), include

https://danielvijge.github.io/lms_mixcloud/public-dev.xml

This Plugin is in Alpha stage and build from the SqueezeCloud Plugin (thanks to the developers), because the documentation
of the LMS Server is very bad and Perl still sucks.
Development builds are only available for Linux. Regular builds are released for Linux,
Windows, and MacOS.

## Licence ##

Expand Down
Loading

0 comments on commit a16c752

Please sign in to comment.