forked from serverkomplex/ShoutcastBridge
Initial commit
commit
f47b51127c
|
@ -0,0 +1,184 @@
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
x64/
|
||||||
|
build/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
|
||||||
|
# Roslyn cache directories
|
||||||
|
*.ide/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
#NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding addin-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
## TODO: Comment the next line if you want to checkin your
|
||||||
|
## web deploy settings but do note that will include unencrypted
|
||||||
|
## passwords
|
||||||
|
*.pubxml
|
||||||
|
|
||||||
|
# NuGet Packages Directory
|
||||||
|
packages/*
|
||||||
|
## TODO: If the tool you use requires repositories.config
|
||||||
|
## uncomment the next line
|
||||||
|
#!packages/repositories.config
|
||||||
|
|
||||||
|
# Enable "build/" folder in the NuGet Packages folder since
|
||||||
|
# NuGet packages use it for MSBuild targets.
|
||||||
|
# This line needs to be after the ignore of the build folder
|
||||||
|
# (and the packages folder if the line above has been uncommented)
|
||||||
|
!packages/build/
|
||||||
|
|
||||||
|
# Windows Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Windows Store app package directory
|
||||||
|
AppPackages/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
sql/
|
||||||
|
*.Cache
|
||||||
|
ClientBin/
|
||||||
|
[Ss]tyle[Cc]op.*
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<solution>
|
||||||
|
<add key="disableSourceControlIntegration" value="true" />
|
||||||
|
</solution>
|
||||||
|
</configuration>
|
Binary file not shown.
|
@ -0,0 +1,144 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
|
||||||
|
|
||||||
|
<!-- Enable the restore command to run before builds -->
|
||||||
|
<RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>
|
||||||
|
|
||||||
|
<!-- Property that enables building a package from a project -->
|
||||||
|
<BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>
|
||||||
|
|
||||||
|
<!-- Determines if package restore consent is required to restore packages -->
|
||||||
|
<RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>
|
||||||
|
|
||||||
|
<!-- Download NuGet.exe if it does not already exist -->
|
||||||
|
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition=" '$(PackageSources)' == '' ">
|
||||||
|
<!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
|
||||||
|
<!-- The official NuGet package source (https://www.nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
|
||||||
|
<!--
|
||||||
|
<PackageSource Include="https://www.nuget.org/api/v2/" />
|
||||||
|
<PackageSource Include="https://my-nuget-source/nuget/" />
|
||||||
|
-->
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
|
||||||
|
<!-- Windows specific commands -->
|
||||||
|
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
|
||||||
|
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
|
||||||
|
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackagesProjectConfig Condition=" '$(OS)' == 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config</PackagesProjectConfig>
|
||||||
|
<PackagesProjectConfig Condition=" '$(OS)' != 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config</PackagesProjectConfig>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackagesConfig Condition="Exists('$(MSBuildProjectDirectory)\packages.config')">$(MSBuildProjectDirectory)\packages.config</PackagesConfig>
|
||||||
|
<PackagesConfig Condition="Exists('$(PackagesProjectConfig)')">$(PackagesProjectConfig)</PackagesConfig>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- NuGet command -->
|
||||||
|
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\NuGet.exe</NuGetExePath>
|
||||||
|
<PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>
|
||||||
|
|
||||||
|
<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
|
||||||
|
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>
|
||||||
|
|
||||||
|
<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
|
||||||
|
|
||||||
|
<RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
|
||||||
|
<NonInteractiveSwitch Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' ">-NonInteractive</NonInteractiveSwitch>
|
||||||
|
|
||||||
|
<PaddedSolutionDir Condition=" '$(OS)' == 'Windows_NT'">"$(SolutionDir) "</PaddedSolutionDir>
|
||||||
|
<PaddedSolutionDir Condition=" '$(OS)' != 'Windows_NT' ">"$(SolutionDir)"</PaddedSolutionDir>
|
||||||
|
|
||||||
|
<!-- Commands -->
|
||||||
|
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)</RestoreCommand>
|
||||||
|
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols</BuildCommand>
|
||||||
|
|
||||||
|
<!-- We need to ensure packages are restored prior to assembly resolve -->
|
||||||
|
<BuildDependsOn Condition="$(RestorePackages) == 'true'">
|
||||||
|
RestorePackages;
|
||||||
|
$(BuildDependsOn);
|
||||||
|
</BuildDependsOn>
|
||||||
|
|
||||||
|
<!-- Make the build depend on restore packages -->
|
||||||
|
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
|
||||||
|
$(BuildDependsOn);
|
||||||
|
BuildPackage;
|
||||||
|
</BuildDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CheckPrerequisites">
|
||||||
|
<!-- Raise an error if we're unable to locate nuget.exe -->
|
||||||
|
<Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
|
||||||
|
<!--
|
||||||
|
Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
|
||||||
|
This effectively acts as a lock that makes sure that the download operation will only happen once and all
|
||||||
|
parallel builds will have to wait for it to complete.
|
||||||
|
-->
|
||||||
|
<MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT;DownloadNuGetExe=$(DownloadNuGetExe)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="_DownloadNuGet">
|
||||||
|
<DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
|
||||||
|
<Exec Command="$(RestoreCommand)"
|
||||||
|
Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />
|
||||||
|
|
||||||
|
<Exec Command="$(RestoreCommand)"
|
||||||
|
LogStandardErrorAsError="true"
|
||||||
|
Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
|
||||||
|
<Exec Command="$(BuildCommand)"
|
||||||
|
Condition=" '$(OS)' != 'Windows_NT' " />
|
||||||
|
|
||||||
|
<Exec Command="$(BuildCommand)"
|
||||||
|
LogStandardErrorAsError="true"
|
||||||
|
Condition=" '$(OS)' == 'Windows_NT' " />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
|
||||||
|
<ParameterGroup>
|
||||||
|
<OutputFilename ParameterType="System.String" Required="true" />
|
||||||
|
</ParameterGroup>
|
||||||
|
<Task>
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Using Namespace="System" />
|
||||||
|
<Using Namespace="System.IO" />
|
||||||
|
<Using Namespace="System.Net" />
|
||||||
|
<Using Namespace="Microsoft.Build.Framework" />
|
||||||
|
<Using Namespace="Microsoft.Build.Utilities" />
|
||||||
|
<Code Type="Fragment" Language="cs">
|
||||||
|
<![CDATA[
|
||||||
|
try {
|
||||||
|
OutputFilename = Path.GetFullPath(OutputFilename);
|
||||||
|
|
||||||
|
Log.LogMessage("Downloading latest version of NuGet.exe...");
|
||||||
|
WebClient webClient = new WebClient();
|
||||||
|
webClient.DownloadFile("https://www.nuget.org/nuget.exe", OutputFilename);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Log.LogErrorFromException(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
]]>
|
||||||
|
</Code>
|
||||||
|
</Task>
|
||||||
|
</UsingTask>
|
||||||
|
</Project>
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 2013
|
||||||
|
VisualStudioVersion = 12.0.21005.1
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShoutcastBridge", "src\sc_bridge\ShoutcastBridge.csproj", "{491CCFC0-2C51-451F-92AE-056B0474AD7E}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{E4EA1463-A0D4-4D06-9F76-8FD55C5797CE}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.nuget\NuGet.Config = .nuget\NuGet.Config
|
||||||
|
.nuget\NuGet.exe = .nuget\NuGet.exe
|
||||||
|
.nuget\NuGet.targets = .nuget\NuGet.targets
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{491CCFC0-2C51-451F-92AE-056B0474AD7E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
|
@ -0,0 +1,46 @@
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
internal class AdminHandler : IHttpRequestHandler
|
||||||
|
{
|
||||||
|
private readonly ShoutcastBridgeServer _shoutcastBridge;
|
||||||
|
|
||||||
|
public AdminHandler(ShoutcastBridgeServer bridgeServer)
|
||||||
|
{
|
||||||
|
_shoutcastBridge = bridgeServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Handle(IHttpContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
string mode;
|
||||||
|
context.Request.QueryString.TryGetByName("mode", out mode);
|
||||||
|
|
||||||
|
switch (mode.ToLower())
|
||||||
|
{
|
||||||
|
case "updinfo":
|
||||||
|
string password;
|
||||||
|
string song;
|
||||||
|
context.Request.QueryString.TryGetByName("pass", out password);
|
||||||
|
context.Request.QueryString.TryGetByName("song", out song);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(password))
|
||||||
|
break;
|
||||||
|
|
||||||
|
context.Response = HttpResponse.CreateWithMessage(HttpResponseCode.Ok, _shoutcastBridge.UpdateMetadata((IPEndPoint)context.RemoteEndPoint, password, song)
|
||||||
|
? "OK"
|
||||||
|
: "Could not update metadata",
|
||||||
|
context.Request.Headers.KeepAliveConnection());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.Response = HttpResponse.CreateWithMessage(HttpResponseCode.Ok, "Unknown mode", context.Request.Headers.KeepAliveConnection());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Task.Factory.GetCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<bridge>
|
||||||
|
|
||||||
|
<!--<ip>0.0.0.0</ip>-->
|
||||||
|
<port>61121</port>
|
||||||
|
|
||||||
|
<stream password="testing0">
|
||||||
|
<port>8000</port>
|
||||||
|
<mountpoint>/stream</mountpoint>
|
||||||
|
</stream>
|
||||||
|
|
||||||
|
<stream password="testing1">
|
||||||
|
<host>internal.listen.rekt.in</host>
|
||||||
|
<port>61120</port>
|
||||||
|
<username>source</username>
|
||||||
|
<password>testing</password>
|
||||||
|
<mountpoint>/afr-herp/live</mountpoint>
|
||||||
|
</stream>
|
||||||
|
|
||||||
|
<stream password="testing2">
|
||||||
|
<host>internal.listen.rekt.in</host>
|
||||||
|
<port>61120</port>
|
||||||
|
<username>source</username>
|
||||||
|
<password>testing</password>
|
||||||
|
<mountpoint>/afr-derp/live</mountpoint>
|
||||||
|
</stream>
|
||||||
|
|
||||||
|
<stream password="testing3">
|
||||||
|
<host>internal.listen.rekt.in</host>
|
||||||
|
<port>61120</port>
|
||||||
|
<username>source</username>
|
||||||
|
<password>testing</password>
|
||||||
|
<mountpoint>/afr-lurk/live</mountpoint>
|
||||||
|
</stream>
|
||||||
|
|
||||||
|
</bridge>
|
|
@ -0,0 +1,176 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Web;
|
||||||
|
using log4net;
|
||||||
|
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public class IcecastWriter
|
||||||
|
{
|
||||||
|
private NetworkStream _ns;
|
||||||
|
private StreamReader _sr;
|
||||||
|
private StreamWriter _sw;
|
||||||
|
private TcpClient _tcp;
|
||||||
|
|
||||||
|
public IcecastWriter()
|
||||||
|
{
|
||||||
|
Hostname = "127.0.0.1";
|
||||||
|
Port = 8000;
|
||||||
|
Username = "source";
|
||||||
|
Password = "hackme";
|
||||||
|
ContentType = "audio/mpeg";
|
||||||
|
Description = "Livestream";
|
||||||
|
Name = "Livestream";
|
||||||
|
Genre = "Various";
|
||||||
|
Public = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Genre { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public ushort KBitrate { get; set; }
|
||||||
|
public bool Public { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
public string Hostname { get; set; }
|
||||||
|
public ushort Port { get; set; }
|
||||||
|
public string Mountpoint { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
public bool Connected
|
||||||
|
{
|
||||||
|
get { return _ns != null && _sr != null && _sw != null && _tcp != null && _tcp.Connected; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Open()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_tcp = new TcpClient(Hostname, Port);
|
||||||
|
_ns = _tcp.GetStream();
|
||||||
|
_sr = new StreamReader(_ns);
|
||||||
|
_sw = new StreamWriter(_ns) {AutoFlush = true};
|
||||||
|
|
||||||
|
// Request headers
|
||||||
|
_sw.WriteLine("SOURCE {0} ICE/1.0", Mountpoint);
|
||||||
|
_sw.WriteLine("content-type: {0}", ContentType);
|
||||||
|
_sw.WriteLine("Authorization: Basic {0}",
|
||||||
|
Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", Username, Password))));
|
||||||
|
_sw.WriteLine("ice-name: {0}", Name);
|
||||||
|
_sw.WriteLine("ice-url: {0}", Url);
|
||||||
|
_sw.WriteLine("ice-genre: {0}", Genre);
|
||||||
|
_sw.WriteLine("ice-bitrate: {0}", KBitrate);
|
||||||
|
_sw.WriteLine("ice-private: {0}", Public ? 0 : 1);
|
||||||
|
_sw.WriteLine("ice-public: {0}", Public ? 1 : 0);
|
||||||
|
_sw.WriteLine("ice-description: {0}", Description);
|
||||||
|
_sw.WriteLine("ice-audio-info: ice-bitrate={0}", KBitrate);
|
||||||
|
_sw.WriteLine();
|
||||||
|
|
||||||
|
// Authorized?
|
||||||
|
string statusLine = _sr.ReadLine();
|
||||||
|
if (statusLine == null)
|
||||||
|
{
|
||||||
|
LogManager.GetLogger("IcecastWriter").ErrorFormat("Icecast socket error: No response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string[] status = statusLine.Split(' ');
|
||||||
|
|
||||||
|
if (status[1] == "200")
|
||||||
|
// Now we can stream
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Something went wrong
|
||||||
|
LogManager.GetLogger("IcecastWriter").ErrorFormat("Icecast HTTP error: {0} {1}", status[1], status[2]);
|
||||||
|
Close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
LogManager.GetLogger("IcecastWriter").ErrorFormat("BUG IN ICECASTWRITING: {0}", error);
|
||||||
|
// Something went wrong
|
||||||
|
Close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Push(byte[] data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_ns == null || _sr == null || _sw == null || _tcp == null || !_tcp.Connected)
|
||||||
|
return;
|
||||||
|
_sw.BaseStream.WriteAsync(data, 0, data.Length);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
LogManager.GetLogger("IcecastWriter").ErrorFormat("Icecast socket error while pushing data: {0}", error);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendMetadata(string song, bool tryOnce = false)
|
||||||
|
{
|
||||||
|
if (song == null)
|
||||||
|
song = string.Empty;
|
||||||
|
|
||||||
|
var reqQuery = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
//{"pass",Password},
|
||||||
|
{"mode", "updinfo"},
|
||||||
|
{"mount", Mountpoint},
|
||||||
|
{"song", song}
|
||||||
|
};
|
||||||
|
|
||||||
|
var reqUriBuilder = new UriBuilder("http", Hostname, Port, "/admin/metadata")
|
||||||
|
{
|
||||||
|
Query =
|
||||||
|
string.Join("&",
|
||||||
|
reqQuery.Keys.Select(
|
||||||
|
key =>
|
||||||
|
string.Format("{0}={1}", HttpUtility.UrlEncode(key),
|
||||||
|
HttpUtility.UrlEncode(reqQuery[key])))),
|
||||||
|
//UserName = Username,
|
||||||
|
//Password = Password
|
||||||
|
};
|
||||||
|
|
||||||
|
var req = (HttpWebRequest) WebRequest.Create(reqUriBuilder.Uri);
|
||||||
|
req.UserAgent = string.Format("ShoutcastBridge/{0}", Assembly.GetExecutingAssembly().GetName().Version);
|
||||||
|
req.Credentials = new NetworkCredential(Username, Password);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
req.GetResponse();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
LogManager.GetLogger("IcecastWriter").WarnFormat("Uri: {0}", reqUriBuilder.Uri);
|
||||||
|
LogManager.GetLogger("IcecastWriter").WarnFormat("Could not send metadata: {0}", error.Message);
|
||||||
|
if (tryOnce)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
return SendMetadata(song, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
LogManager.GetLogger("IcecastWriter").Info("Disconnecting");
|
||||||
|
if (_ns == null || _sr == null || _sw == null || _tcp == null || !_tcp.Connected)
|
||||||
|
return;
|
||||||
|
_sr.Dispose();
|
||||||
|
_sw.Dispose();
|
||||||
|
_ns.Dispose();
|
||||||
|
_tcp.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using log4net;
|
||||||
|
using log4net.Appender;
|
||||||
|
using log4net.Config;
|
||||||
|
using log4net.Core;
|
||||||
|
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
private static ILog _log;
|
||||||
|
static int Main(string[] args)
|
||||||
|
{
|
||||||
|
var configFile = "Config.xml";
|
||||||
|
|
||||||
|
// Arguments
|
||||||
|
if (args.Any())
|
||||||
|
{
|
||||||
|
configFile = args[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logging
|
||||||
|
BasicConfigurator.Configure(new ConsoleAppender()
|
||||||
|
{
|
||||||
|
Layout = new log4net.Layout.PatternLayout(@"[%logger] %level: %message%newline"),
|
||||||
|
Threshold = Level.Debug
|
||||||
|
});
|
||||||
|
_log = LogManager.GetLogger("bridgeapp");
|
||||||
|
|
||||||
|
// Configuration setup
|
||||||
|
XDocument xdoc;
|
||||||
|
if (!File.Exists(configFile))
|
||||||
|
{
|
||||||
|
_log.Error("Invalid configuration file given (file not found).");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
xdoc = XDocument.Load(configFile);
|
||||||
|
if (xdoc.Root == null)
|
||||||
|
{
|
||||||
|
_log.Error("Invalid configuration file given (could not parse file, no XML root found).");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_log.Error("Invalid configuration file given (could not parse file, any typos in the XML code?).");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ip address to work with
|
||||||
|
var ip = IPAddress.Any;
|
||||||
|
var ipElements = xdoc.Root.Elements("ip").ToArray();
|
||||||
|
if (ipElements.Any())
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(ipElements.First().Value, out ip))
|
||||||
|
{
|
||||||
|
_log.Error("Invalid IP address in configuration, please check the configuration file.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (ipElements.Count() > 1)
|
||||||
|
_log.Warn("Multiple ip addresses defined, using first one. Please check the configuration file as soon as possible and fix this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get port to work with
|
||||||
|
var port = (ushort)8000;
|
||||||
|
var portElements = xdoc.Root.Elements("port").ToArray();
|
||||||
|
if (portElements.Any())
|
||||||
|
{
|
||||||
|
if (!ushort.TryParse(portElements.First().Value, out port))
|
||||||
|
{
|
||||||
|
_log.Error("Invalid port in configuration, please check the configuration file.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (portElements.Count() > 1)
|
||||||
|
_log.Warn("Multiple ports defined, using first one. Please check the configuration file as soon as possible and fix this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new ShoutcastBridgeServer(ip, port);
|
||||||
|
|
||||||
|
// Mountpoint configuration
|
||||||
|
var mpElements = xdoc.Root.Descendants("stream").ToArray();
|
||||||
|
if (!mpElements.Any())
|
||||||
|
{
|
||||||
|
_log.Error("No mountpoints/streams configured. Please define at least one in the configuration file.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
foreach (var mpElement in mpElements)
|
||||||
|
{
|
||||||
|
var pwAttr = mpElement.Attribute("password");
|
||||||
|
string pw;
|
||||||
|
if (pwAttr == null || string.IsNullOrEmpty(pw = pwAttr.Value))
|
||||||
|
{
|
||||||
|
_log.Error("Mountpoint without password defined. All mountpoints need a unique password.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (mpElements.Count(m => m.Attribute("password").Value == pwAttr.Value) > 1)
|
||||||
|
{
|
||||||
|
_log.ErrorFormat("Too many mountpoints with same password ({0}) defined. All mountpoints need a unique password.", pwAttr.Value);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ushort parsedPort;
|
||||||
|
var mpPortElements = mpElement.Elements("port").ToArray();
|
||||||
|
if (!mpPortElements.Any())
|
||||||
|
{
|
||||||
|
//mpElement.Add(new XElement("port", 8000));
|
||||||
|
//mpPortElements = mpElement.Elements("port").ToArray();
|
||||||
|
parsedPort = 8000;
|
||||||
|
}
|
||||||
|
else if (mpPortElements.Count() > 1)
|
||||||
|
{
|
||||||
|
_log.WarnFormat("Too many icecast ports defined for mountpoint pw={0}, please only define one.", pw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (!ushort.TryParse(mpPortElements.Single().Value, out parsedPort))
|
||||||
|
{
|
||||||
|
_log.ErrorFormat("Invalid icecast port defined for mountpoint pw={0}, please define a proper port number.", pw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpHostElements = mpElement.Elements("host").ToArray();
|
||||||
|
if (!mpHostElements.Any())
|
||||||
|
{
|
||||||
|
mpElement.Add(new XElement("host", IPAddress.Loopback.ToString()));
|
||||||
|
mpHostElements = mpElement.Elements("host").ToArray();
|
||||||
|
}
|
||||||
|
else if (mpHostElements.Count() > 1)
|
||||||
|
{
|
||||||
|
_log.WarnFormat("Too many icecast host names defined for mountpoint pw={0}, please only define one.", pw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpMountpointElements = mpElement.Elements("mountpoint").ToArray();
|
||||||
|
if (!mpMountpointElements.Any())
|
||||||
|
{
|
||||||
|
mpElement.Add(new XElement("mountpoint", "/stream"));
|
||||||
|
mpMountpointElements = mpElement.Elements("mountpoint").ToArray(); // yes, this is rude.
|
||||||
|
}
|
||||||
|
else if (mpMountpointElements.Count() > 1)
|
||||||
|
{
|
||||||
|
_log.WarnFormat(
|
||||||
|
"Too many icecast mountpoint names defined for mountpoint pw={0}, please only define one.", pw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpPasswordElements = mpElement.Elements("password").ToArray();
|
||||||
|
if (!mpPasswordElements.Any())
|
||||||
|
{
|
||||||
|
mpElement.Add(new XElement("password", "hackme"));
|
||||||
|
mpPasswordElements = mpElement.Elements("password").ToArray();
|
||||||
|
}
|
||||||
|
else if (mpPasswordElements.Count() > 1)
|
||||||
|
{
|
||||||
|
_log.WarnFormat("Too many icecast passwords defined for mountpoint pw={0}, please only define one.", pw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpUsernameElements = mpElement.Elements("username").ToArray();
|
||||||
|
if (!mpUsernameElements.Any())
|
||||||
|
{
|
||||||
|
mpElement.Add(new XElement("username", "source"));
|
||||||
|
mpUsernameElements = mpElement.Elements("username").ToArray();
|
||||||
|
}
|
||||||
|
else if (mpUsernameElements.Count() > 1)
|
||||||
|
{
|
||||||
|
_log.WarnFormat("Too many icecast usernames defined for mountpoint pw={0}, please only define one.", pw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AddMountpoint(pw, new ShoutcastBridgeMountpoint()
|
||||||
|
{
|
||||||
|
IcecastHost = mpHostElements.Single().Value,
|
||||||
|
IcecastPort = parsedPort,
|
||||||
|
IcecastMountpoint = mpMountpointElements.Single().Value,
|
||||||
|
IcecastUsername = mpUsernameElements.Single().Value,
|
||||||
|
IcecastPassword = mpPasswordElements.Single().Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.DebugFormat("Starting up Shoutcast bridge now.");
|
||||||
|
sb.Start();
|
||||||
|
|
||||||
|
return 0; // We'll never get here anyways... except on errors `-`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// Allgemeine Informationen über eine Assembly werden über die folgenden
|
||||||
|
// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
|
||||||
|
// die mit einer Assembly verknüpft sind.
|
||||||
|
[assembly: AssemblyTitle("AFR.ShoutcastBridge")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("Hewlett-Packard")]
|
||||||
|
[assembly: AssemblyProduct("AFR.ShoutcastBridge")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © Hewlett-Packard 2014")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar
|
||||||
|
// für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von
|
||||||
|
// COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird
|
||||||
|
[assembly: Guid("bf523a9d-260e-4949-bbdd-0c0acccc84da")]
|
||||||
|
|
||||||
|
// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
|
||||||
|
//
|
||||||
|
// Hauptversion
|
||||||
|
// Nebenversion
|
||||||
|
// Buildnummer
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern
|
||||||
|
// übernehmen, indem Sie "*" eingeben:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public delegate string ShoutcastAuthenticationEventHandler(string password);
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{491CCFC0-2C51-451F-92AE-056B0474AD7E}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>AFR.ShoutcastBridge</RootNamespace>
|
||||||
|
<AssemblyName>sc_bridge</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<TargetFrameworkProfile />
|
||||||
|
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
|
||||||
|
<RestorePackages>true</RestorePackages>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutDir>$(SolutionDir)\bin\$(Configuration)\$(Platform)\</OutDir>
|
||||||
|
<IntDir>$(SolutionDir)\obj\$(TargetName)\$(Configuration)\$(Platform)\</IntDir>
|
||||||
|
<IntermediateOutputPath>$(SolutionDir)\obj\$(TargetName)\$(Configuration)\$(Platform)\</IntermediateOutputPath>
|
||||||
|
<BaseIntermediateOutputPath>$(SolutionDir)\obj\$(TargetName)\$(Configuration)\$(Platform)\</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath>$(SolutionDir)\bin\$(Configuration)\$(Platform)\</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
<HintPath>..\packages\log4net.2.0.3\lib\net40-full\log4net.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
<HintPath>..\packages\Newtonsoft.Json.6.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Web" />
|
||||||
|
<Reference Include="System.XML" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="uhttpsharp">
|
||||||
|
<HintPath>..\packages\uHttpSharp.0.1.4.7\lib\net40\uhttpsharp.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="AdminHandler.cs" />
|
||||||
|
<Compile Include="IcecastWriter.cs" />
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="ShoutcastAuthenticationEventHandler.cs" />
|
||||||
|
<Compile Include="ShoutcastBridgeServer.cs" />
|
||||||
|
<Compile Include="ShoutcastBridgeMountpoint.cs" />
|
||||||
|
<Compile Include="ShoutcastDataEventHandler.cs" />
|
||||||
|
<Compile Include="ShoutcastDisconnectEventHandler.cs" />
|
||||||
|
<Compile Include="ShoutcastReadingStream.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="app.config" />
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Config.xml">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
||||||
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
<Target Name="BeforeBuild">
|
||||||
|
</Target>
|
||||||
|
<Target Name="AfterBuild">
|
||||||
|
</Target>
|
||||||
|
-->
|
||||||
|
</Project>
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public class ShoutcastBridgeMountpoint
|
||||||
|
{
|
||||||
|
public string IcecastHost { get; set; }
|
||||||
|
public ushort IcecastPort { get; set; }
|
||||||
|
public string IcecastMountpoint { get; set; }
|
||||||
|
public string IcecastUsername { get; set; }
|
||||||
|
public string IcecastPassword { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public class ShoutcastBridgeServer
|
||||||
|
{
|
||||||
|
private readonly ILog _log;
|
||||||
|
private readonly ILog _adminlog;
|
||||||
|
private readonly ILog _sourcelog;
|
||||||
|
|
||||||
|
private TcpListener _sourceServer;
|
||||||
|
|
||||||
|
private HttpServer _adminServer;
|
||||||
|
|
||||||
|
private readonly IPAddress _ip;
|
||||||
|
|
||||||
|
private readonly ushort _port;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, ShoutcastBridgeMountpoint> _mountpoints =
|
||||||
|
new Dictionary<string, ShoutcastBridgeMountpoint>();
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Tuple<ShoutcastReadingStream, IcecastWriter>> _connectedMountpoints =
|
||||||
|
new Dictionary<string, Tuple<ShoutcastReadingStream, IcecastWriter>>();
|
||||||
|
|
||||||
|
public ShoutcastBridgeServer(IPAddress ip, ushort port)
|
||||||
|
{
|
||||||
|
_ip = ip;
|
||||||
|
_port = port;
|
||||||
|
_log = LogManager.GetLogger("ShoutcastBridge");
|
||||||
|
_adminlog = LogManager.GetLogger("Shout1Admin");
|
||||||
|
_sourcelog = LogManager.GetLogger("Shout1Source");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_log.DebugFormat("Starting SCv1 source server on port {0}...", _port + 1);
|
||||||
|
_sourceServer = new TcpListener(_ip, _port + 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_sourceServer.Start();
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
_log.ErrorFormat("Could not start up source server. {0}", error.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.DebugFormat("Starting SC admin HTTP server on port {0}...", _port);
|
||||||
|
_adminServer = new HttpServer(new HttpRequestProvider());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_adminServer.Use(new TcpListenerAdapter(new TcpListener(_ip, _port)));
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
_log.ErrorFormat("Could not start up admin HTTP server. {0}", error.Message);
|
||||||
|
}
|
||||||
|
_adminServer.Use(new AdminHandler(this));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_adminServer.Start();
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
_log.ErrorFormat("Could not start up admin HTTP server. {0}", error.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info("Shoutcast bridge started up successfully, now ready.");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Socket sock;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sock = _sourceServer.AcceptSocket();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Debug("Accepting incoming connection");
|
||||||
|
var ns = new NetworkStream(sock, true);
|
||||||
|
var srs = new ShoutcastReadingStream((IPEndPoint)sock.RemoteEndPoint, ns);
|
||||||
|
srs.Authenticating += password =>
|
||||||
|
{
|
||||||
|
if (!_mountpoints.ContainsKey(password))
|
||||||
|
{
|
||||||
|
_sourcelog.DebugFormat("[{0}] Connection declined: Invalid password", srs.ClientEndPoint);
|
||||||
|
return "Invalid password";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connectedMountpoints.ContainsKey(password))
|
||||||
|
{
|
||||||
|
_sourcelog.DebugFormat("[{0}] Connection declined: Stream already in use", srs.ClientEndPoint);
|
||||||
|
return "Stream already in use";
|
||||||
|
}
|
||||||
|
|
||||||
|
var mp = _mountpoints[password];
|
||||||
|
|
||||||
|
var writer = new IcecastWriter()
|
||||||
|
{
|
||||||
|
Hostname = mp.IcecastHost,
|
||||||
|
Port = mp.IcecastPort,
|
||||||
|
Mountpoint = mp.IcecastMountpoint,
|
||||||
|
Username = string.IsNullOrEmpty(mp.IcecastUsername) ? "source" : mp.IcecastUsername,
|
||||||
|
Password = mp.IcecastPassword,
|
||||||
|
ContentType = "undefined" /* will be defined on first packet */
|
||||||
|
};
|
||||||
|
|
||||||
|
_connectedMountpoints.Add(password, new Tuple<ShoutcastReadingStream, IcecastWriter>(srs, writer));
|
||||||
|
|
||||||
|
|
||||||
|
_sourcelog.DebugFormat("[{0}] Testing connection to master server...", srs.ClientEndPoint);
|
||||||
|
if (!writer.Open())
|
||||||
|
{
|
||||||
|
_sourcelog.DebugFormat("[{0}] Connection declined: Master server was unavailable", srs.ClientEndPoint);
|
||||||
|
return "Master server unavailable, try again later.";
|
||||||
|
}
|
||||||
|
writer.Close();
|
||||||
|
|
||||||
|
_sourcelog.DebugFormat("[{0}] Connection accepted", srs.ClientEndPoint);
|
||||||
|
return "OK2";
|
||||||
|
};
|
||||||
|
srs.Disconnected += () =>
|
||||||
|
{
|
||||||
|
_sourcelog.DebugFormat("[{0}] Disconnected", srs.ClientEndPoint);
|
||||||
|
var pws = _connectedMountpoints.Where(s => s.Value.Item1 == srs).Select(s => s.Key).ToArray();
|
||||||
|
if (!pws.Any())
|
||||||
|
{
|
||||||
|
_sourcelog.ErrorFormat("[{0}] Could not find any connected mountpoints associated with this IP, dangling connection! This is a bug!", srs.ClientEndPoint);
|
||||||
|
}
|
||||||
|
foreach (var pw in pws.ToArray())
|
||||||
|
{
|
||||||
|
_connectedMountpoints[pw].Item2.Close(); // close Icecast connection
|
||||||
|
_connectedMountpoints.Remove(pw); // remove from registered connections
|
||||||
|
}
|
||||||
|
};
|
||||||
|
srs.ReceivedData += data =>
|
||||||
|
{
|
||||||
|
var conns = _connectedMountpoints.Where(s => s.Value.Item1 == srs).ToArray();
|
||||||
|
if (!conns.Any())
|
||||||
|
{
|
||||||
|
_sourcelog.ErrorFormat("Dangling source connection from {0} - nothing bound to it!", srs.ClientEndPoint);
|
||||||
|
}
|
||||||
|
foreach (var conn in conns)
|
||||||
|
{
|
||||||
|
var icecast = conn.Value.Item2;
|
||||||
|
var shoutcast = conn.Value.Item1;
|
||||||
|
|
||||||
|
if (!icecast.Connected)
|
||||||
|
{
|
||||||
|
_sourcelog.InfoFormat("[{0}] Connecting to {1}:{2}{3}...", srs.ClientEndPoint, icecast.Hostname, icecast.Port, icecast.Mountpoint);
|
||||||
|
icecast.Name = icecast.Description = shoutcast.StreamName;
|
||||||
|
icecast.Genre = shoutcast.StreamGenre;
|
||||||
|
icecast.KBitrate = shoutcast.StreamKBitrate;
|
||||||
|
icecast.Url = shoutcast.StreamUrl;
|
||||||
|
icecast.Public = shoutcast.StreamPublic;
|
||||||
|
if (icecast.ContentType == "undefined")
|
||||||
|
switch (BitConverter.ToString(data.Take(2).ToArray()).Replace("-", "").ToLower())
|
||||||
|
{
|
||||||
|
case "4f67": // OGG container
|
||||||
|
_sourcelog.DebugFormat("[{0}] Detected OGG audio container", srs.ClientEndPoint);
|
||||||
|
icecast.ContentType = "audio/ogg";
|
||||||
|
break;
|
||||||
|
case "fff9": // AAC
|
||||||
|
_sourcelog.DebugFormat("[{0}] Detected AAC-LC data", srs.ClientEndPoint);
|
||||||
|
icecast.ContentType = "audio/aac";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_sourcelog.DebugFormat("[{0}] Assuming MP3 codec", srs.ClientEndPoint);
|
||||||
|
icecast.ContentType = "audio/mpeg";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!icecast.Open())
|
||||||
|
{
|
||||||
|
_sourcelog.ErrorFormat("[{0}] Could not connect with Icecast, retrying on next packet", srs.ClientEndPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_sourcelog.InfoFormat("[{0}] Connected!", srs.ClientEndPoint);
|
||||||
|
// TODO: Sync metadata after connection immediately!
|
||||||
|
}
|
||||||
|
|
||||||
|
icecast.Push(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
srs.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_adminServer.Dispose();
|
||||||
|
_sourceServer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMountpoint(string password, ShoutcastBridgeMountpoint mp)
|
||||||
|
{
|
||||||
|
_log.DebugFormat("Adding mountpoint: {0}:{1}, mountpoint \"{2}\"", mp.IcecastHost, mp.IcecastPort, mp.IcecastMountpoint);
|
||||||
|
_mountpoints.Add(password, mp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateMetadata(IPEndPoint remoteEndPoint, string password, string song)
|
||||||
|
{
|
||||||
|
// Is stream connected?
|
||||||
|
if (!_connectedMountpoints.ContainsKey(password))
|
||||||
|
{
|
||||||
|
_adminlog.DebugFormat("[{0}] Metadata update declined: Invalid password", remoteEndPoint);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorize client - requirement: same IP
|
||||||
|
if (!_connectedMountpoints[password].Item1.ClientEndPoint.Address.Equals(remoteEndPoint.Address))
|
||||||
|
{
|
||||||
|
_adminlog.DebugFormat("[{0}] Metadata update declined: IP mismatch", remoteEndPoint);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally update metadata
|
||||||
|
_adminlog.DebugFormat("[{0}] Metadata update: {1}", remoteEndPoint, song);
|
||||||
|
return _connectedMountpoints[password].Item2.SendMetadata(song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public delegate void ShoutcastDataEventHandler(byte[] data);
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public delegate void ShoutcastDisconnectEventHandler();
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AFR.ShoutcastBridge
|
||||||
|
{
|
||||||
|
public class ShoutcastReadingStream
|
||||||
|
{
|
||||||
|
private readonly NetworkStream _ns;
|
||||||
|
private readonly StreamReader _sr;
|
||||||
|
private readonly StreamWriter _sw;
|
||||||
|
private CancellationTokenSource _cancel;
|
||||||
|
private Task _task;
|
||||||
|
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public string StreamName
|
||||||
|
{
|
||||||
|
get { return _headers.ContainsKey("icy-name") ? _headers["icy-name"] : string.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StreamGenre
|
||||||
|
{
|
||||||
|
get { return _headers.ContainsKey("icy-genre") ? _headers["icy-genre"] : string.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StreamPublic
|
||||||
|
{
|
||||||
|
get { return _headers.ContainsKey("icy-pub") && int.Parse(_headers["icy-pub"]) >= 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ushort StreamKBitrate
|
||||||
|
{
|
||||||
|
get { return _headers.ContainsKey("icy-br") ? ushort.Parse(_headers["icy-br"]) : (ushort)0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StreamUrl
|
||||||
|
{
|
||||||
|
get { return _headers.ContainsKey("icy-url") ? _headers["icy-url"] : string.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StreamIrc
|
||||||
|
{
|
||||||
|
get { return _headers.ContainsKey("icy-irc") ? _headers["icy-irc"] : string.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPEndPoint ClientEndPoint { get; private set; }
|
||||||
|
|
||||||
|
public ShoutcastReadingStream(IPEndPoint ep, NetworkStream ns)
|
||||||
|
{
|
||||||
|
ClientEndPoint = ep;
|
||||||
|
_ns = ns;
|
||||||
|
_task = null;
|
||||||
|
_sr = new StreamReader(ns);
|
||||||
|
_sw = new StreamWriter(ns) {AutoFlush = true};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_cancel = new CancellationTokenSource();
|
||||||
|
_task = Task.Factory.StartNew(_start, _cancel.Token).ContinueWith(task =>
|
||||||
|
{
|
||||||
|
if (Disconnected != null)
|
||||||
|
Disconnected();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void _start()
|
||||||
|
{
|
||||||
|
var password = _sr.ReadLine();
|
||||||
|
|
||||||
|
// Check if any password given
|
||||||
|
if (password == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if password correct
|
||||||
|
password = password.TrimEnd('\n', '\r');
|
||||||
|
var status = Authenticating(password);
|
||||||
|
if (Authenticating != null && status != "OK2")
|
||||||
|
{
|
||||||
|
_sw.WriteLine(status);
|
||||||
|
_ns.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sw.WriteLine(status);
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
var line = _sr.ReadLine();
|
||||||
|
while (line != null && line.Trim().Any())
|
||||||
|
{
|
||||||
|
var lineSplit = line.Split(':');
|
||||||
|
if (lineSplit.Length < 2)
|
||||||
|
continue; // Invalid header line
|
||||||
|
|
||||||
|
var name = lineSplit[0];
|
||||||
|
var value = string.Join(":", lineSplit.Skip(1));
|
||||||
|
|
||||||
|
if (_headers.ContainsKey(name))
|
||||||
|
_headers[name] = value;
|
||||||
|
else
|
||||||
|
_headers.Add(name, value);
|
||||||
|
|
||||||
|
line = _sr.ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
var data = new byte[2048];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int length;
|
||||||
|
while ((length = _ns.Read(data, 0, data.Length)) > 0)
|
||||||
|
{
|
||||||
|
_cancel.Token.ThrowIfCancellationRequested();
|
||||||
|
if (ReceivedData != null)
|
||||||
|
ReceivedData(data.Take(length).ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
LogManager.GetLogger("ShoutRead").Warn("Exception in reading loop while reading. Interpreting as disconnection.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (_cancel == null || _task == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_cancel.Cancel();
|
||||||
|
_task.Wait();
|
||||||
|
_task = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
~ShoutcastReadingStream()
|
||||||
|
{
|
||||||
|
_sr.Dispose();
|
||||||
|
_sw.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event ShoutcastDisconnectEventHandler Disconnected;
|
||||||
|
public event ShoutcastAuthenticationEventHandler Authenticating;
|
||||||
|
public event ShoutcastDataEventHandler ReceivedData;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.5.0.0" newVersion="4.5.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /></startup></configuration>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="log4net" version="2.0.3" targetFramework="net45" />
|
||||||
|
<package id="Newtonsoft.Json" version="6.0.3" targetFramework="net45" />
|
||||||
|
<package id="uHttpSharp" version="0.1.4.7" targetFramework="net40" />
|
||||||
|
</packages>
|
Loading…
Reference in New Issue