├── .gitignore ├── LICENSE ├── MpdDj ├── MpdDj.sln └── MpdDj │ ├── Commands.cs │ ├── Configuration.cs │ ├── Downloaders.cs │ ├── MPC.cs │ ├── MpdDj.csproj │ ├── Program.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── packages.config ├── README.md └── python ├── .gitignore ├── cmdlistener.py ├── djimporter.py ├── matrix_client ├── mmdj.py └── mpc ├── __init__.py └── mpc.py /.gitignore: -------------------------------------------------------------------------------- 1 | #User Specific 2 | *.userprefs 3 | *.usertasks 4 | 5 | #Mono Project Files 6 | *.pidb 7 | *.resources 8 | test-results/ 9 | 10 | ## Ignore Visual Studio temporary files, build results, and 11 | ## files generated by popular Visual Studio add-ons. 12 | 13 | # User-specific files 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | 34 | # Visual Studio 2015 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # MSTest test Results 40 | [Tt]est[Rr]esult*/ 41 | [Bb]uild[Ll]og.* 42 | 43 | # NUNIT 44 | *.VisualState.xml 45 | TestResult.xml 46 | 47 | # Build Results of an ATL Project 48 | [Dd]ebugPS/ 49 | [Rr]eleasePS/ 50 | dlldata.c 51 | 52 | # DNX 53 | project.lock.json 54 | artifacts/ 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opendb 89 | *.opensdf 90 | *.sdf 91 | *.cachefile 92 | *.VC.db 93 | *.VC.VC.opendb 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.pfx 201 | *.publishsettings 202 | node_modules/ 203 | orleans.codegen.cs 204 | 205 | # Since there are multiple workflows, uncomment next line to ignore bower_components 206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 207 | #bower_components/ 208 | 209 | # RIA/Silverlight projects 210 | Generated_Code/ 211 | 212 | # Backup & report files from converting an old project file 213 | # to a newer Visual Studio version. Backup files are not needed, 214 | # because we have git ;-) 215 | _UpgradeReport_Files/ 216 | Backup*/ 217 | UpgradeLog*.XML 218 | UpgradeLog*.htm 219 | 220 | # SQL Server files 221 | *.mdf 222 | *.ldf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | 238 | # Visual Studio 6 build log 239 | *.plg 240 | 241 | # Visual Studio 6 workspace options file 242 | *.opt 243 | 244 | # Visual Studio LightSwitch build output 245 | **/*.HTMLClient/GeneratedArtifacts 246 | **/*.DesktopClient/GeneratedArtifacts 247 | **/*.DesktopClient/ModelManifest.xml 248 | **/*.Server/GeneratedArtifacts 249 | **/*.Server/ModelManifest.xml 250 | _Pvt_Extensions 251 | 252 | # Paket dependency manager 253 | .paket/paket.exe 254 | paket-files/ 255 | 256 | # FAKE - F# Make 257 | .fake/ 258 | 259 | # JetBrains Rider 260 | .idea/ 261 | *.sln.iml 262 | 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Will Hunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MpdDj/MpdDj.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MpdDj", "MpdDj\MpdDj.csproj", "{9840E711-4380-43B5-89D9-6243E051BCB8}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixSDK", "..\..\matrix-dotnet-sdk\MatrixSDK\MatrixSDK\MatrixSDK.csproj", "{BF7A3DBC-EFBD-43D2-8609-CCAB4A9DFA07}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9840E711-4380-43B5-89D9-6243E051BCB8}.Debug|x86.ActiveCfg = Debug|x86 15 | {9840E711-4380-43B5-89D9-6243E051BCB8}.Debug|x86.Build.0 = Debug|x86 16 | {9840E711-4380-43B5-89D9-6243E051BCB8}.Release|x86.ActiveCfg = Release|x86 17 | {9840E711-4380-43B5-89D9-6243E051BCB8}.Release|x86.Build.0 = Release|x86 18 | {BF7A3DBC-EFBD-43D2-8609-CCAB4A9DFA07}.Debug|x86.ActiveCfg = Debug|Any CPU 19 | {BF7A3DBC-EFBD-43D2-8609-CCAB4A9DFA07}.Debug|x86.Build.0 = Debug|Any CPU 20 | {BF7A3DBC-EFBD-43D2-8609-CCAB4A9DFA07}.Release|x86.ActiveCfg = Release|Any CPU 21 | {BF7A3DBC-EFBD-43D2-8609-CCAB4A9DFA07}.Release|x86.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/Commands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Newtonsoft.Json.Linq; 8 | using MatrixSDK.Client; 9 | using MatrixSDK.Structures; 10 | using System.Reflection; 11 | namespace MpdDj 12 | { 13 | [AttributeUsage(AttributeTargets.Method)] 14 | public class BotCmd : Attribute{ 15 | public readonly string CMD; 16 | public readonly string[] BeginsWith; 17 | public BotCmd(string cmd,params string[] beginswith){ 18 | CMD = cmd; 19 | BeginsWith = beginswith; 20 | } 21 | } 22 | 23 | [AttributeUsage(AttributeTargets.Method)] 24 | public class BotFallback : Attribute { 25 | 26 | } 27 | 28 | [AttributeUsage(AttributeTargets.Method)] 29 | public class BotHelp : Attribute { 30 | public readonly string HelpText; 31 | public BotHelp(string help){ 32 | HelpText = help; 33 | } 34 | } 35 | 36 | public class Commands 37 | { 38 | //[BotCmd("shuffle")] 39 | //public static void Shuffle(string cmd,string sender, MatrixRoom room){ 40 | // Console.WriteLine("Shuffle"); 41 | //} 42 | 43 | [BotCmd("ping")] 44 | [BotHelp("Ping the server and get the delay.")] 45 | public static void Ping(string cmd, string sender, MatrixRoom room){ 46 | room.SendMessage ("Pong at" + DateTime.Now.ToLongTimeString()); 47 | } 48 | 49 | [BotCmd("current")] 50 | [BotHelp("Get the current song title.")] 51 | public static void GetSongName(string cmd,string sender,MatrixRoom room){ 52 | MPCCurrentSong song = Program.MPCClient.CurrentSong (); 53 | Program.MPCClient.Status(); 54 | if (song.file != null) { 55 | FileInfo file = new FileInfo (song.file); 56 | string name = file.Name.Replace (file.Extension, ""); 57 | name = new string(System.Text.Encoding.UTF8.GetChars (Convert.FromBase64String (name))); 58 | 59 | string[] time = Program.MPCClient.lastStatus.time.Split(':'); 60 | int elapsed = int.Parse(time[0]); 61 | int total = int.Parse(time[1]); 62 | name += String.Format(" {0}:{1}/{2}:{3}", elapsed/60,elapsed%60,total/60,total%60); 63 | 64 | room.SendNotice (name); 65 | } else { 66 | room.SendNotice ("Nothing is currently playing"); 67 | } 68 | } 69 | 70 | 71 | [BotCmd("next")] 72 | [BotHelp("Skip current song.")] 73 | public static void NextTrack(string cmd, string sender, MatrixRoom room){ 74 | Program.MPCClient.Next (); 75 | } 76 | 77 | [BotCmd("help")] 78 | [BotHelp("This help text.")] 79 | public static void Help(string cmd, string sender, MatrixRoom room){ 80 | string helptext = ""; 81 | foreach(MethodInfo method in typeof(Commands).GetMethods(BindingFlags.Static|BindingFlags.Public)){ 82 | BotCmd c = method.GetCustomAttribute (); 83 | BotHelp h= method.GetCustomAttribute (); 84 | 85 | if (c != null) { 86 | helptext += String.Format("

{0} {1}

",c.CMD, h != null ? System.Web.HttpUtility.HtmlEncode(h.HelpText) : ""); 87 | } 88 | } 89 | MMessageCustomHTML htmlmsg = new MMessageCustomHTML(); 90 | htmlmsg.body = helptext.Replace("","").Replace("","").Replace("

","").Replace("

","\n"); 91 | htmlmsg.formatted_body = helptext; 92 | room.SendMessage(htmlmsg); 93 | } 94 | 95 | [BotCmd("search")] 96 | [BotFallback()] 97 | [BotHelp("Get the first youtube result by keywords.")] 98 | public static void SearchYTForTrack(string cmd, string sender, MatrixRoom room){ 99 | string query = cmd.Replace("search ",""); 100 | if(string.IsNullOrWhiteSpace(query)){ 101 | return; 102 | } 103 | try 104 | { 105 | string url = Downloaders.GetYoutubeURLFromSearch(query); 106 | if(url != null){ 107 | DownloadTrack(url,sender,room); 108 | } 109 | else 110 | { 111 | throw new Exception("No videos matching those terms were found"); 112 | } 113 | } 114 | catch(Exception e){ 115 | room.SendNotice ("There was an issue with that request, "+sender+": " + e.Message); 116 | Console.Error.WriteLine (e); 117 | } 118 | 119 | } 120 | 121 | [BotCmd("[url]","http://","https://","youtube.com","youtu.be","soundcloud.com")] 122 | [BotHelp("Type a youtube/soundcloud/file url in to add to the playlist.")] 123 | public static void DownloadTrack(string cmd, string sender, MatrixRoom room) 124 | { 125 | try 126 | { 127 | List videos = new List(); 128 | if (Downloaders.YoutubeGetIDFromURL (cmd) != "") { 129 | videos = DownloadYoutube (cmd, sender, room); 130 | } 131 | else if(Uri.IsWellFormedUriString (cmd, UriKind.Absolute)){ 132 | videos = new List(); 133 | videos.Add(DownloadGeneric (cmd, sender, room)); 134 | } 135 | else 136 | { 137 | room.SendNotice ("Sorry, that type of URL isn't supported right now :/"); 138 | return; 139 | } 140 | 141 | Program.MPCClient.RequestLibraryUpdate(); 142 | //Program.MPCClient.Idle("update");//Wait for it to start 143 | Program.MPCClient.Idle("update");//Wait for it to finish 144 | 145 | foreach(string[] res in videos){ 146 | Program.MPCClient.AddFile(res[0]); 147 | } 148 | 149 | Program.MPCClient.Status(); 150 | 151 | #if DEBUG 152 | Console.WriteLine(JObject.FromObject(Program.MPCClient.lastStatus)); 153 | #endif 154 | 155 | int position = Program.MPCClient.lastStatus.playlistlength; 156 | if(position == 1){ 157 | Program.MPCClient.Play(); 158 | room.SendNotice("Started playing " + videos[0][1] + " | " + Configuration.Config["mpc"]["streamurl"]); 159 | } 160 | else 161 | { 162 | room.SendNotice(videos[0][1] + " has been queued at position "+position+"."); 163 | } 164 | 165 | } 166 | catch(Exception e){ 167 | room.SendNotice ("There was an issue with that request, "+sender+": " + e.Message); 168 | Console.Error.WriteLine (e); 169 | } 170 | } 171 | 172 | public static string[] DownloadGeneric(string cmd, string sender, MatrixRoom room){ 173 | Uri uri; 174 | if (!Uri.TryCreate (cmd, UriKind.Absolute,out uri)) { 175 | throw new Exception ("Not a url :("); 176 | } 177 | FileInfo info = new FileInfo (uri.Segments.Last ()); 178 | string filename = Convert.ToBase64String(Encoding.UTF8.GetBytes(info.Name))+info.Extension; 179 | Downloaders.GenericDownload (cmd, filename); 180 | return new string[2] {filename, uri.Segments.Last ()}; 181 | } 182 | 183 | public static List DownloadYoutube(string cmd, string sender, MatrixRoom room){ 184 | JObject[] videos = Downloaders.YoutubeGetData (cmd); 185 | List output = new List(videos.Length); 186 | List tasks = new List (); 187 | foreach(JObject data in videos){ 188 | //Check Length 189 | int seconds = data["duration"].ToObject(); 190 | int max = int.Parse(Configuration.Config["youtube"]["maxlength"]); 191 | if(seconds > max){ 192 | throw new Exception("Video exceeds duration limit of " + Math.Round(max / 60f,1) + " minutes"); 193 | } 194 | string filename = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data["fulltitle"].ToObject())); 195 | Task t = new Task( () => {Downloaders.YoutubeDownload(data["webpage_url"].ToObject(),filename);}); 196 | t.Start (); 197 | tasks.Add(t); 198 | output.Add(new string[2]{filename + ".ogg",data["title"].ToObject()}); 199 | } 200 | System.Threading.Tasks.Task.WaitAll (tasks.ToArray(),TimeSpan.FromSeconds(20*videos.Length)); 201 | return output; 202 | } 203 | 204 | [BotCmd("stream")] 205 | [BotHelp("Get the url of the stream.")] 206 | public static void StreamUrl(string cmd, string sender, MatrixRoom room){ 207 | room.SendNotice(Configuration.Config["mpc"]["streamurl"]); 208 | } 209 | 210 | [BotCmd("lyrics")] 211 | [BotHelp("Search by lyric")] 212 | public static void LyricSearch(string cmd, string sender, MatrixRoom room){ 213 | string suggestion = Downloaders.GetSongNameByLyric(cmd.Replace("lyrics ","")); 214 | if(suggestion == null){ 215 | room.SendNotice("I couldn't find any songs with that lyric :("); 216 | } 217 | else 218 | { 219 | room.SendNotice(String.Format("Matched '{0}'. Checking Youtube for it",suggestion)); 220 | SearchYTForTrack("search "+suggestion,sender,room); 221 | } 222 | } 223 | 224 | [BotCmd("lyric")] 225 | [BotHelp("You fear the letter 's'? Aww babe, I'll fix that for you <3")] 226 | public static void LyricSearchAlias(string cmd, string sender, MatrixRoom room){ 227 | LyricSearch(cmd,sender,room); 228 | } 229 | 230 | [BotCmd("playlist")] 231 | [BotHelp("Display the the shortened playlist.")] 232 | public static void PlaylistDisplay(string cmd, string sender, MatrixRoom room){ 233 | string[] files = Program.MPCClient.Playlist (); 234 | string output = "▶ "; 235 | if (files.Length > 0) { 236 | for (int i = 0; i < files.Length; i++) { 237 | if (i > 4) 238 | break; 239 | string file = files [i].Substring (0, files [i].Length - 4) + '\n';//Remove the extension 240 | file = new string(System.Text.Encoding.UTF8.GetChars (Convert.FromBase64String (file))); 241 | output += file + "\n"; 242 | } 243 | 244 | room.SendMessage (output); 245 | } else { 246 | room.SendNotice ("The playlist is empty"); 247 | } 248 | } 249 | 250 | 251 | 252 | } 253 | } 254 | 255 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using IniParser; 4 | using IniParser.Model; 5 | namespace MpdDj 6 | { 7 | public class Configuration 8 | { 9 | public static IniData Config { get; private set; } 10 | public static IniData DefaultConfiguration(){ 11 | IniData defaultData = new IniData (); 12 | SectionData MPC = new SectionData ("mpc"); 13 | SectionData Matrix = new SectionData ("matrix"); 14 | 15 | defaultData.Sections.Add (MPC); 16 | defaultData.Sections.Add (Matrix); 17 | 18 | MPC.Keys.AddKey("host","localhost"); 19 | MPC.Keys.AddKey("port","6600"); 20 | MPC.Keys.AddKey("streamurl","http://localhost:8000"); 21 | MPC.Keys.AddKey("music_dir","/var/lib/mpd/music"); 22 | 23 | Matrix.Keys.AddKey("host","https://localhost:8448"); 24 | Matrix.Keys.AddKey("user","username"); 25 | Matrix.Keys.AddKey("pass","password"); 26 | Matrix.Keys.AddKey("rooms","#RoomA,#RoomB:localhost,#RoomC"); 27 | return defaultData; 28 | } 29 | 30 | public static void ReadConfig(string cfgpath){ 31 | if (File.Exists (cfgpath)) { 32 | FileIniDataParser parser = new FileIniDataParser (); 33 | Config = parser.ReadFile (cfgpath); 34 | } else { 35 | Console.WriteLine ("[Warn] The config file could not be found. Using defaults"); 36 | Config = DefaultConfiguration (); 37 | } 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/Downloaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Text.RegularExpressions; 4 | using System.Net.Http; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using System.Linq; 8 | using System.Xml; 9 | using Newtonsoft.Json.Linq; 10 | namespace MpdDj 11 | { 12 | public class Downloaders 13 | { 14 | const string YT_BIN_WRAP = "/usr/bin/youtube-dl"; 15 | const string FFMPEG_BIN_WRAP = "/usr/bin/ffmpeg"; 16 | //const string YT_FFMPEG = " -i \"{0}.{2}\" -vn -y -c:a libvorbis -b:a 192k \"{1}.ogg\""; 17 | const string YT_YOUTUBEDL = " -x --audio-format \"vorbis\" -f best -o '{0}.%(ext)s' {1}"; 18 | static readonly Regex YoutubeRegex = new Regex("youtu(?:\\.be|be\\.com)/(?:.*v(?:/|=)|(?:.*/)?)([a-zA-Z0-9-_]+)",RegexOptions.Compiled); 19 | static readonly Regex YoutubePLRegex = new Regex ("^.*(youtu.be\\/|list=)([^#\\&\\?]*).*", RegexOptions.Compiled); 20 | static readonly Regex SoundcloudRegex = new Regex ("^https?:\\/\\/(soundcloud.com|snd.sc)\\/(.*)$", RegexOptions.Compiled); 21 | 22 | 23 | public static string GetSongNameByLyric(string lyric){ 24 | const string URL = "http://api.chartlyrics.com/apiv1.asmx/SearchLyricText?lyricText={0}"; 25 | string finalUrl = string.Format(URL,Uri.EscapeUriString(lyric)); 26 | string result = ""; 27 | using (System.Net.WebClient client = new System.Net.WebClient ()) { 28 | result = client.DownloadString(finalUrl); 29 | } 30 | XmlDocument doc = new XmlDocument(); 31 | doc.LoadXml(result); 32 | XmlElement firstSong = (XmlElement)doc.ChildNodes[1].FirstChild; 33 | if((firstSong).GetAttribute("xsi:nil") != ""){ 34 | return null; 35 | } 36 | else 37 | { 38 | string artist = firstSong.GetElementsByTagName("Artist")[0].InnerText; 39 | string song = firstSong.GetElementsByTagName("Song")[0].InnerText; 40 | return string.Format("{0} {1}",artist,song); 41 | } 42 | 43 | 44 | } 45 | 46 | public static void GenericDownload(string url,string filename){ 47 | string[] allowedMimetypes = Configuration.Config ["file"] ["mimetypes"].Split (' '); 48 | using (HttpClient client = new HttpClient ()) { 49 | Task msg = client.SendAsync (new HttpRequestMessage (HttpMethod.Head, url)); 50 | msg.Wait (); 51 | IEnumerable types; 52 | IEnumerable lengths; 53 | 54 | if (msg.Result.StatusCode != System.Net.HttpStatusCode.OK) { 55 | throw new Exception ("Server gave a " + msg.Result.StatusCode); 56 | } 57 | 58 | if (msg.Result.Content.Headers.TryGetValues ("Content-Type", out types)) { 59 | if (!types.Any (x => allowedMimetypes.Contains (x))) { 60 | throw new Exception ("Filetype not supported"); 61 | } 62 | if (msg.Result.Content.Headers.TryGetValues ("Content-Length", out lengths)) { 63 | float length = int.Parse (lengths.First()) / (float)Math.Pow(1024,2); 64 | float maxlength = float.Parse (Configuration.Config ["file"] ["size_limit"]); 65 | if (length > maxlength) { 66 | throw new Exception ("File is over " + maxlength + "MBs in size"); 67 | } 68 | } else { 69 | throw new Exception ("Cannot gauge content size from headers. Bailing"); 70 | } 71 | } 72 | else 73 | { 74 | throw new Exception("Server does not state a Content-Type. Bailing."); 75 | } 76 | } 77 | 78 | 79 | using (System.Net.WebClient client = new System.Net.WebClient ()) { 80 | try { 81 | string fname = System.IO.Path.Combine(Configuration.Config ["mpc"] ["music_dir"],filename); 82 | System.IO.FileInfo finfo = new System.IO.FileInfo(fname); 83 | if(!client.DownloadFileTaskAsync (url,finfo.FullName).Wait(TimeSpan.FromSeconds(15))){ 84 | throw new Exception("File took too long to download"); 85 | } 86 | } catch (Exception e) { 87 | Console.WriteLine ("Issue downloading file", e); 88 | throw new Exception ("Couldn't download file"); 89 | } 90 | } 91 | } 92 | 93 | public static string GetYoutubeURLFromSearch(string terms){ 94 | const string URL_FORMAT = "https://www.googleapis.com/youtube/v3/search?part=snippet&q={0}&type=video&maxResults=1&key={1}&videoCategoryId=10"; 95 | string url = string.Format(URL_FORMAT,Uri.EscapeUriString(terms),Configuration.Config["youtube"]["apikey"]); 96 | JObject obj; 97 | using (System.Net.WebClient client = new System.Net.WebClient ()) { 98 | try { 99 | string data = client.DownloadString(url); 100 | obj = JObject.Parse(data); 101 | } catch (Exception e) { 102 | Console.WriteLine ("Issue with YT API", e); 103 | throw new Exception ("Couldn't do search."); 104 | } 105 | } 106 | JToken[] items = obj.GetValue("items").ToArray(); 107 | if(items.Length == 0){ 108 | return null; 109 | } 110 | else 111 | { 112 | return "https://youtube.com/watch?v=" + items[0].Value("id").Value("videoId"); 113 | } 114 | } 115 | 116 | public static string YoutubeGetIDFromURL(string url) 117 | { 118 | GroupCollection regg = YoutubeRegex.Match (url).Groups; 119 | GroupCollection reggpl = YoutubePLRegex.Match (url).Groups; 120 | if (regg.Count > 1 && regg[1].Value != "playlist") { 121 | return regg [1].Value; 122 | } else if (reggpl.Count > 2) { 123 | return reggpl [2].Value; 124 | } else if (SoundcloudRegex.IsMatch (url)){ 125 | return url; 126 | } else { 127 | return ""; 128 | } 129 | } 130 | 131 | // private static void YoutubeConvert(string filename){ 132 | // string[] extensions = new string[2]{"mp4","webm"}; 133 | // //It doesn't tell us :/ 134 | // string extension = null; 135 | // foreach (string ext in extensions) { 136 | // if (System.IO.File.Exists ("/tmp/" + filename + "." + ext)) { 137 | // extension = ext; 138 | // } 139 | // } 140 | // 141 | // if(extension == null){ 142 | // throw new Exception ("Couldn't find video file"); 143 | // } 144 | // 145 | // 146 | // Process proc = new Process (); 147 | // proc.StartInfo = new ProcessStartInfo () { 148 | // FileName = FFMPEG_BIN_WRAP, 149 | // WorkingDirectory = "/tmp", 150 | // Arguments = String.Format(YT_FFMPEG,filename,Configuration.Config["mpc"]["music_dir"] + "/" + filename,extension), 151 | // UseShellExecute = false, 152 | // LoadUserProfile = true, 153 | // }; 154 | // proc.Start (); 155 | // proc.WaitForExit (); 156 | // if(proc.ExitCode != 0){ 157 | // throw new Exception("There was an error transcoding the video"); 158 | // } 159 | // System.IO.File.Delete("/tmp/"+filename+"."+extension); 160 | // } 161 | 162 | public static void YoutubeDownload(string url, string filename){ 163 | Process proc = new Process (); 164 | proc.StartInfo = new ProcessStartInfo () { 165 | FileName = YT_BIN_WRAP, 166 | WorkingDirectory = Configuration.Config["mpc"]["music_dir"] , 167 | Arguments = String.Format(YT_YOUTUBEDL,filename,url), 168 | UseShellExecute = false, 169 | LoadUserProfile = true, 170 | RedirectStandardOutput = true, 171 | }; 172 | proc.Start (); 173 | proc.WaitForExit (); 174 | if(proc.ExitCode != 0){ 175 | throw new Exception("There was an error downloading/transcoding the video"); 176 | } 177 | //YoutubeConvert (filename); 178 | } 179 | 180 | public static JObject[] YoutubeGetData(string url){ 181 | string id = YoutubeGetIDFromURL (url); 182 | if (id == "") { 183 | throw new Exception ("Bad url."); 184 | } 185 | Process proc = new Process (); 186 | proc.StartInfo = new ProcessStartInfo () { 187 | FileName = YT_BIN_WRAP, 188 | Arguments = "-j " + id, 189 | UseShellExecute = false, 190 | LoadUserProfile = true, 191 | RedirectStandardOutput = true, 192 | 193 | }; 194 | proc.Start (); 195 | string data = ""; 196 | while (!proc.HasExited) { 197 | data += proc.StandardOutput.ReadToEnd (); 198 | System.Threading.Thread.Sleep (50); 199 | } 200 | proc.Dispose (); 201 | if (data == "") { 202 | throw new Exception ("Bad url."); 203 | } 204 | List videos = new List (); 205 | foreach(string line in data.Split('\n')){ 206 | if(string.IsNullOrWhiteSpace(line)) 207 | continue; 208 | videos.Add(JObject.Parse(line)); 209 | } 210 | return videos.ToArray (); 211 | } 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/MPC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Net.NetworkInformation; 7 | using System.Linq; 8 | using System.Threading; 9 | namespace MpdDj 10 | { 11 | public struct MPCCurrentSong 12 | { 13 | public string file; 14 | public DateTime date; 15 | public string Artist; 16 | public string Title; 17 | public string Album; 18 | public string Track; 19 | public string Date; 20 | public string Genre; 21 | public string Disc; 22 | public int Time; 23 | public int Pos; 24 | public int Id; 25 | } 26 | 27 | public struct MPCStatus 28 | { 29 | public int volume; 30 | public bool repeat; 31 | public bool random; 32 | public bool single; 33 | public bool consume; 34 | public int playlist; 35 | public int playlistlength; 36 | public float mixrampdb; 37 | public string state; 38 | public int song; 39 | public int songid; 40 | public string time; 41 | public float elapsed; 42 | public int bitrate; 43 | public string audio; 44 | public int nextsong; 45 | public int nextsongid; 46 | } 47 | 48 | public class MPC 49 | { 50 | TcpClient client; 51 | NetworkStream stream; 52 | public MPCStatus lastStatus { get; private set; } 53 | int port; 54 | string host; 55 | Mutex client_mutex; 56 | public MPC (string host, string port) 57 | { 58 | this.host = host; 59 | this.port = int.Parse (port); 60 | client_mutex = new Mutex (); 61 | OpenConnection (); 62 | } 63 | 64 | private T FillStruct(string data){ 65 | string[] lines = data.Split ('\n'); 66 | object o = Activator.CreateInstance (); 67 | foreach (string line in lines) { 68 | string[] keyval = line.Split (new string[1]{": "}, 2,StringSplitOptions.RemoveEmptyEntries); 69 | if (keyval.Length == 2) { 70 | System.Reflection.FieldInfo f = typeof(T).GetField (keyval [0]); 71 | if (f != null) { 72 | object value; 73 | if (f.FieldType == typeof(float)) { 74 | value = float.Parse (keyval [1]); 75 | } else if (f.FieldType == typeof(int)) { 76 | value = int.Parse (keyval [1]); 77 | } else if (f.FieldType == typeof(bool)) { 78 | value = int.Parse (keyval [1]) == 1; 79 | } else if (f.FieldType == typeof(DateTime)) { 80 | value = DateTime.Parse (keyval [1]); 81 | } else { 82 | value = keyval [1]; 83 | } 84 | f.SetValue(o,value); 85 | } 86 | } 87 | } 88 | return (T)o; 89 | } 90 | 91 | private void OpenConnection(){ 92 | client = new TcpClient (host,port); 93 | stream = client.GetStream (); 94 | byte[] buff = new byte[6]; 95 | stream.Read (buff, 0, buff.Length); 96 | if (Encoding.UTF8.GetString (buff) != "OK MPD") { 97 | throw new Exception ("Connection is not a MPD stream"); 98 | } 99 | //Eat the rest 100 | while (stream.DataAvailable) { 101 | stream.ReadByte (); 102 | } 103 | } 104 | 105 | private string Send(string data, bool wait = false){ 106 | if (!client_mutex.WaitOne (30000)) { 107 | throw new Exception ("Timed out waiting for the mutex to become avaliable for the mpc client."); 108 | } 109 | OpenConnection (); 110 | 111 | byte[] bdata = Encoding.UTF8.GetBytes (data+"\n"); 112 | stream.Write (bdata,0,bdata.Length); 113 | List buffer = new List(); 114 | while (!stream.DataAvailable && wait) { 115 | System.Threading.Thread.Sleep (100); 116 | } 117 | 118 | while (stream.DataAvailable) { 119 | buffer.Add ((byte)stream.ReadByte ()); 120 | } 121 | string sbuffer = Encoding.UTF8.GetString (buffer.ToArray()); 122 | 123 | client.Close (); 124 | stream.Dispose (); 125 | client_mutex.ReleaseMutex (); 126 | return sbuffer; 127 | } 128 | 129 | public void Play(){ 130 | Send ("play"); 131 | } 132 | 133 | public void Next(){ 134 | Send ("next"); 135 | } 136 | 137 | public void Previous(){ 138 | Send ("prev"); 139 | } 140 | 141 | public void AddFile(string file){ 142 | Send ("add " + file); 143 | } 144 | 145 | public void Idle(string subsystem){ 146 | Send ("idle " + subsystem,true); 147 | } 148 | 149 | public string[] Playlist(){ 150 | string playlist = Send ("playlist",true); 151 | List newsongs = new List (); 152 | foreach (string song in playlist.Split ('\n')) { 153 | int indexof = song.IndexOf (' '); 154 | if (indexof != -1) { 155 | newsongs.Add(song.Substring (indexof + 1)); 156 | } 157 | } 158 | return newsongs.ToArray(); 159 | } 160 | 161 | public void Status(){ 162 | string sstatus = Send ("status",true); 163 | 164 | MPCStatus status = FillStruct (sstatus); 165 | lastStatus = status; 166 | } 167 | 168 | public MPCCurrentSong CurrentSong(){ 169 | string result = Send("currentsong",true); 170 | MPCCurrentSong song = FillStruct(result); 171 | return song; 172 | } 173 | 174 | public void RequestLibraryUpdate(){ 175 | Send ("update"); 176 | } 177 | } 178 | 179 | public static class TcpExtensions{ 180 | public static TcpState GetState(this TcpClient tcpClient) 181 | { 182 | var foo = IPGlobalProperties.GetIPGlobalProperties() 183 | .GetActiveTcpConnections() 184 | .SingleOrDefault(x => x.LocalEndPoint.Equals(tcpClient.Client.LocalEndPoint)); 185 | return foo != null ? foo.State : TcpState.Unknown; 186 | } 187 | } 188 | } 189 | 190 | 191 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/MpdDj.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | {9840E711-4380-43B5-89D9-6243E051BCB8} 7 | Exe 8 | MpdDj 9 | MpdDj 10 | v4.5 11 | 12 | 13 | true 14 | full 15 | false 16 | bin\Debug 17 | DEBUG; 18 | prompt 19 | 4 20 | true 21 | x86 22 | 23 | 24 | full 25 | true 26 | bin\Release 27 | prompt 28 | 4 29 | true 30 | x86 31 | 32 | 33 | 34 | 35 | ..\packages\ini-parser.2.2.4\lib\net20\INIFileParser.dll 36 | 37 | 38 | ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {BF7A3DBC-EFBD-43D2-8609-CCAB4A9DFA07} 59 | MatrixSDK 60 | 61 | 62 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using MatrixSDK.Client; 7 | using MatrixSDK.Structures; 8 | namespace MpdDj 9 | { 10 | class Program 11 | { 12 | public static MatrixClient Client; 13 | public static MPC MPCClient; 14 | public static Dictionary Cmds = new Dictionary(); 15 | public static MethodInfo fallback = null; 16 | public static void Main (string[] args) 17 | { 18 | Console.WriteLine ("Reading INI File"); 19 | string cfgpath; 20 | 21 | if (args.Length > 1) { 22 | cfgpath = args [1]; 23 | } else { 24 | cfgpath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.config/mpddj.ini"; 25 | } 26 | cfgpath = System.IO.Path.GetFullPath (cfgpath); 27 | Console.WriteLine("Trying to read from " + cfgpath); 28 | Configuration.ReadConfig (cfgpath); 29 | 30 | 31 | Console.WriteLine ("Connecting to MPD"); 32 | MPCClient = new MPC (Configuration.Config ["mpc"] ["host"], Configuration.Config ["mpc"] ["port"]); 33 | MPCClient.Status (); 34 | 35 | 36 | Console.WriteLine ("Connecting to Matrix"); 37 | Client = new MatrixClient (Configuration.Config ["matrix"] ["host"]); 38 | 39 | Console.WriteLine("Connected. Logging in"); 40 | Client.LoginWithPassword (Configuration.Config ["matrix"] ["user"], Configuration.Config ["matrix"] ["pass"]); 41 | Console.WriteLine("Logged in OK"); 42 | Console.WriteLine("Joining Rooms:"); 43 | foreach (string roomid in Configuration.Config ["matrix"] ["rooms"].Split(',')) { 44 | MatrixRoom room = Client.GetRoomByAlias (roomid); 45 | if (room == null) { 46 | room = Client.JoinRoom (roomid); 47 | if (room != null) { 48 | Console.WriteLine ("\tJoined " + roomid); 49 | room.OnMessage += Room_OnMessage; 50 | } else { 51 | Console.WriteLine ("\tCouldn't find " + roomid); 52 | } 53 | } else { 54 | room.OnMessage += Room_OnMessage; 55 | } 56 | } 57 | Console.WriteLine ("Done!"); 58 | //Find commands 59 | foreach(MethodInfo method in typeof(Commands).GetMethods(BindingFlags.Static|BindingFlags.Public)){ 60 | BotCmd cmd = method.GetCustomAttribute (); 61 | if (cmd != null) { 62 | Cmds.Add (cmd, method); 63 | } 64 | if(method.GetCustomAttribute() != null){ 65 | if(fallback != null){ 66 | Console.WriteLine("WARN: You have more than one fallback command set, overwriting previous"); 67 | } 68 | fallback = method; 69 | } 70 | } 71 | 72 | } 73 | 74 | static void Room_OnMessage (MatrixRoom room, MatrixSDK.Structures.MatrixEvent evt) 75 | { 76 | if (evt.age > 3000) { 77 | return; // Too old 78 | } 79 | 80 | string msg = ((MatrixMRoomMessage)evt.content).body; 81 | 82 | if (msg.StartsWith ("!mpddj")) { 83 | msg = msg.Substring (7); 84 | string[] parts = msg.Split (' '); 85 | string cmd = parts [0].ToLower (); 86 | try 87 | { 88 | MethodInfo method = Cmds.First(x => { 89 | return (x.Key.CMD == cmd) || ( x.Key.BeginsWith.Any( y => cmd.StartsWith(y) )); 90 | }).Value; 91 | 92 | Task task = new Task (() => { 93 | method.Invoke (null, new object[3]{ msg, evt.sender, room }); 94 | }); 95 | task.Start (); 96 | } 97 | catch(InvalidOperationException){ 98 | Task task = new Task (() => { 99 | fallback.Invoke (null, new object[3]{ msg, evt.sender, room }); 100 | }); 101 | task.Start (); 102 | } 103 | catch(Exception e){ 104 | Console.Error.WriteLine ("Problem with one of the commands"); 105 | Console.Error.WriteLine (e); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | 7 | [assembly: AssemblyTitle ("MpdDj")] 8 | [assembly: AssemblyDescription ("")] 9 | [assembly: AssemblyConfiguration ("")] 10 | [assembly: AssemblyCompany ("")] 11 | [assembly: AssemblyProduct ("")] 12 | [assembly: AssemblyCopyright ("will")] 13 | [assembly: AssemblyTrademark ("")] 14 | [assembly: AssemblyCulture ("")] 15 | 16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 19 | 20 | [assembly: AssemblyVersion ("1.0.*")] 21 | 22 | // The following attributes are used to specify the signing key for the assembly, 23 | // if desired. See the Mono documentation for more information about signing. 24 | 25 | //[assembly: AssemblyDelaySign(false)] 26 | //[assembly: AssemblyKeyFile("")] 27 | 28 | -------------------------------------------------------------------------------- /MpdDj/MpdDj/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix-mpd-dj 2 | A matrix bot for controlling a mpd music stream 3 | 4 | Setup 5 | ----- 6 | You need: 7 | - A working MPD Setup 8 | - MPC installed 9 | - A http stream 10 | - A folder that this script can write and read to for music files. 11 | - Auto update turned on in mpd 12 | 13 | Commands 14 | -------- 15 | The current command selection is listed below: 16 | ``` 17 | play - Play if the stream has stopped 18 | prev - Go to the previous track 19 | next - Go to the next track 20 | current - Current track name 21 | help - List avaliable commands 22 | [youtube url] - Give a youtube url to queue it 23 | stream url - What is the stream url? 24 | update - Refresh the library if the mpd fails to find a uploaded track. 25 | ``` 26 | 27 | 28 | Config 29 | ------ 30 | The configuration is stored in ~/.config/mpddj.ini 31 | 32 | The default config is listed below: 33 | ``` 34 | [mpc] 35 | host = localhost 36 | port = 6600 37 | streamurl = http://localhost:8000 38 | [matrix] 39 | host = https://localhost:8448 40 | user = username 41 | pass = password 42 | rooms = #RoomA,#RoomB:localhost,#RoomC 43 | ``` 44 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /python/cmdlistener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from djimporter import download_youtube 4 | from matrix_client.client import MatrixClient 5 | from mpc.mpc import MPCClient 6 | from time import time, sleep 7 | from queue import Queue 8 | import traceback 9 | 10 | 11 | class CmdListener: 12 | rooms = {} 13 | mpc = None 14 | client = None 15 | stream_url = "" 16 | cmd_queue = None 17 | music_dir = None 18 | def __init__(self,config): 19 | self.mpc = MPCClient(config["mpc"]["host"],config["mpc"]["port"]) 20 | self.music_dir = config["mpc"]["music_dir"] 21 | self.cmd_queue = Queue() 22 | 23 | try: 24 | self.mpc.current() 25 | except: 26 | raise Exception("An error occured while connecting to the mpd server.") 27 | return 28 | 29 | try: 30 | self.client = MatrixClient(config["matrix"]["host"]) 31 | except: 32 | raise Exception("An error occured while connecting to the matrix server!") 33 | return 34 | 35 | 36 | self.stream_url = config["mpc"]["streamurl"] 37 | try: 38 | self.client.login_with_password(config["matrix"]["user"],config["matrix"]["pass"]) 39 | except MatrixRequestError as e: 40 | print(e) 41 | if e.code == 403: 42 | print("Bad username or password.") 43 | sys.exit(4) 44 | else: 45 | print("Check your sever details are correct.") 46 | sys.exit(3) 47 | 48 | MTX_ROOMS = config["matrix"]["rooms"].split(",") 49 | 50 | for sroom in MTX_ROOMS: 51 | room = self.client.join_room(sroom) 52 | room.add_listener(self.__on_cmd) 53 | self.rooms[room.room_id] = room 54 | 55 | def run(self): 56 | self.client.start_listener_thread() 57 | while True: 58 | event = self.cmd_queue.get() 59 | if event is None: 60 | continue; 61 | else: 62 | cmd = event['content']['body'] 63 | body = cmd.lower() 64 | if body.startswith('mpddj:') or body.startswith('!mpddj'): 65 | self.__parse_command(body[6:],event,cmd[6:]) 66 | elif body.startswith('mpd dj:'): 67 | self.__parse_command(body[7:],event,cmd[7:]) 68 | 69 | def __on_cmd(self,event): 70 | if event['type'] == "m.room.message" and event['content']['msgtype'] == "m.text": 71 | if event['age'] < 5000: 72 | self.cmd_queue.put(event) 73 | 74 | def __newfile_play(self,fname,max_attempts=25): 75 | # Do update check 76 | attempts = 0 77 | gotfile = False 78 | while attempts < max_attempts and not gotfile: 79 | musiclist = self.mpc.listall() 80 | gotfile = fname in musiclist 81 | if not gotfile: 82 | sleep(0.5) 83 | attempts += 1 84 | if gotfile: 85 | self.mpc.add(fname) 86 | self.mpc.play() 87 | 88 | def __parse_command(self,cmd,event,cmd_regular): 89 | cmd = cmd.strip() 90 | parts = cmd.split(" ") 91 | room = self.rooms[event['room_id']]; 92 | if parts[0] == "shuffle": 93 | self.mpc.shuffle() 94 | elif parts[0] == "prev": 95 | self.mpc.next() 96 | elif parts[0] == "play": 97 | self.mpc.play() 98 | elif parts[0] == "next": 99 | self.mpc.next() 100 | elif parts[0] == "playlist": 101 | plist = self.mpc.playlist().split("\n")[:-1][:3] 102 | if len(plist) > 0: 103 | plist[0] = "▶ " + plist[0] 104 | room.send_text("\n".join(plist).replace(".ogg","")) 105 | else: 106 | room.send_text("The playlist is empty") 107 | elif parts[0] == "current": 108 | fname = self.mpc.current() 109 | fname = fname.replace("_"," ").replace(".ogg","") 110 | room.send_text(fname) 111 | elif parts[0] == "update": 112 | self.mpc.update() 113 | elif parts[0] == "help": 114 | room.send_text("Commands are: play,prev,next,current,playlist,help,[youtube url],stream url") 115 | elif "youtube.com/" in parts[0] or "soundcloud.com/" in parts[0]: 116 | pos = 1 117 | try: 118 | url = cmd_regular.strip().split(" ")[0] 119 | pos = len(self.mpc.playlist().split('\n'))-1 120 | status,fname = download_youtube(url,self.music_dir,self.__newfile_play) 121 | except Exception as e: 122 | print(e) 123 | print(traceback.format_exc()) 124 | room.send_text("Couldn't download the file :(") 125 | return; 126 | self.mpc.update(True) 127 | 128 | 129 | if status: 130 | if fname is not False: 131 | fi = fname.replace(".ogg","") 132 | if pos > 0: 133 | room.send_text(fi + " has been queued. It currently at position "+str(pos+1)) 134 | else: 135 | room.send_text(fi + " has begun playing") 136 | else: 137 | if pos > 1: 138 | room.send_text("Your playlist has been queued. It currently at position "+str(pos)) 139 | else: 140 | room.send_text("Your playlist has begun playing") 141 | if self.mpc.current() == '': 142 | sleep(0.5)# Allow it to breathe 143 | self.mpc.play() 144 | else: 145 | room.send_text("I couldn't play the file. This is probably a bug and should be reported to Half-Shot.") 146 | 147 | elif "stream url" in cmd: 148 | room.send_text(self.stream_url) 149 | -------------------------------------------------------------------------------- /python/djimporter.py: -------------------------------------------------------------------------------- 1 | #from mutagen.oggvorbis import OggVorbis 2 | import youtube_dl 3 | from os.path import getctime, basename,exists 4 | from glob import iglob 5 | from threading import Lock 6 | from time import sleep 7 | 8 | yt_mutex = Lock() 9 | __yt_callback = None 10 | __yt_lastfile = "" 11 | def yt_hook(status): 12 | global __yt_callback 13 | global __yt_lastfile 14 | if status['status'] == "finished": 15 | print("Finished downloading video") 16 | fname = status['filename'].replace(".tmp",".ogg") 17 | print("YT Last file:",__yt_lastfile) 18 | if __yt_lastfile != "": 19 | __yt_callback(__yt_lastfile) 20 | __yt_lastfile = basename(fname) 21 | 22 | def download_youtube(url,outputdir,callback): 23 | global __yt_callback 24 | global __yt_lastfile 25 | yt_mutex.acquire() 26 | path = False 27 | status = False 28 | __yt_callback = callback 29 | try: 30 | ydl_opts = { 31 | 'format': 'bestaudio/best', 32 | 'outtmpl': outputdir+'%(title)s.tmp', 33 | 'add-metadata':True, 34 | 'postprocessors': [{ 35 | 'key': 'FFmpegExtractAudio', 36 | 'preferredcodec': 'vorbis', 37 | 'preferredquality': '192', 38 | }], 39 | 'progress_hooks':[yt_hook] 40 | } 41 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 42 | 43 | data = ydl.extract_info(url,False) 44 | if 'entries' not in data.keys(): 45 | path = basename(ydl.prepare_filename(data).replace(".tmp",".ogg")) 46 | status = ydl.download([url]) 47 | print(status) 48 | status = (status == 0) 49 | __yt_callback(__yt_lastfile) 50 | __yt_lastfile = "" 51 | finally: 52 | yt_mutex.release() 53 | return status, path 54 | -------------------------------------------------------------------------------- /python/matrix_client: -------------------------------------------------------------------------------- 1 | matrix-python-sdk/matrix_client -------------------------------------------------------------------------------- /python/mmdj.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ Matrix MPD DJ """ 3 | import configparser 4 | from os.path import expanduser,exists 5 | from cmdlistener import CmdListener 6 | from sys import exit 7 | # Error Codes: 8 | # 1 - Unknown problem has occured 9 | # 2 - Could not find the server. 10 | # 3 - Bad URL Format. 11 | # 4 - Bad username/password. 12 | # 11 - Wrong room format. 13 | # 12 - Couldn't find room. 14 | 15 | def default_config(path): 16 | config = configparser.ConfigParser() 17 | 18 | config["mpc"] = {} 19 | config["matrix"] = {} 20 | 21 | config["mpc"]["host"] = "localhost" 22 | config["mpc"]["port"] = "6600" 23 | config["mpc"]["streamurl"] = "http://localhost:8000" 24 | config["mpc"]["music_dir"] = "/var/lib/mpd/music" 25 | 26 | config["matrix"]["host"] = "https://localhost:8448" 27 | config["matrix"]["user"] = "username" 28 | config["matrix"]["pass"] = "password" 29 | config["matrix"]["rooms"] = "#RoomA,#RoomB:localhost,#RoomC" 30 | 31 | with open(path, 'w') as configfile: 32 | config.write(configfile) 33 | 34 | def read_config(path): 35 | config = configparser.ConfigParser() 36 | config.read(path) 37 | if "mpc" not in config.keys(): 38 | print("Error, missing mpc section") 39 | return False 40 | 41 | keys = ["host","port","streamurl","music_dir"] 42 | for key in keys: 43 | if key not in config["mpc"].keys(): 44 | print("Error, missing",key,"from mpc section") 45 | return False 46 | 47 | if "matrix" not in config.keys(): 48 | print("Error, missing matrix section") 49 | return False 50 | 51 | keys = ["host","user","pass","rooms"] 52 | for key in keys: 53 | if key not in config["matrix"].keys(): 54 | print("Error, missing",key,"from matrix section") 55 | return False 56 | 57 | return True 58 | 59 | #Get config 60 | cfgfile = expanduser("~/.config/mpddj.ini") 61 | config = None 62 | if not exists(cfgfile): 63 | print("Config file not found, writing a new one") 64 | print("Writing to",cfgfile) 65 | config = default_config(cfgfile) 66 | else: 67 | print("Reading",cfgfile) 68 | if read_config(cfgfile): 69 | config = configparser.ConfigParser() 70 | config.read(cfgfile) 71 | else: 72 | print("Cannot start, you have errors in your config file") 73 | 74 | try: 75 | cmd = CmdListener(config) 76 | except Exception as e: 77 | print("Failed to connect to one or more services.") 78 | print("The message was:",e) 79 | exit(2) 80 | cmd.run() 81 | 82 | 83 | MPD_HOST = "localhost" 84 | MPD_STREAMURL = "http://half-shot.uk:8000/mpd.ogg" 85 | MTX_HOST = "https://souppenguin.com:8448" 86 | MTX_USERNAME = "@mpddj:souppenguin.com" 87 | MTX_ROOMS = ["#devtest:souppenguin.com","#offtopic:matrix.org"] 88 | -------------------------------------------------------------------------------- /python/mpc/ __init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | -------------------------------------------------------------------------------- /python/mpc/mpc.py: -------------------------------------------------------------------------------- 1 | from subprocess import call, check_output 2 | import shlex 3 | import socket 4 | #TODO: Implement the rest 5 | 6 | def onoff(val): 7 | if val == True: 8 | return "on" 9 | else: 10 | return "off" 11 | 12 | class MPCClient: 13 | def __init__(self,hostname="localhost",port="6600"): 14 | self.host = hostname 15 | self.port = port 16 | 17 | def __runcmd(self,cmd,output=False,args=[]): 18 | c = ["mpc","-h",self.host,"-p",self.port,cmd] 19 | if(output): 20 | return check_output(c+args).decode() 21 | else: 22 | return call(c+args) 23 | 24 | def __runtcp(self,cmd,output=False,args=[]): 25 | BUFFER = 512 26 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 27 | try: 28 | s.connect((self.host,int(self.port))) 29 | except: 30 | raise Exception("Couldn't connect to MPD") 31 | 32 | data = s.recv(BUFFER).decode() 33 | if not data.startswith("OK MPD"): 34 | s.close() 35 | raise Exception("Couldn't connect to MPD") 36 | msg = cmd+" "+" ".join(args)+"\n" 37 | s.send(msg.encode()) 38 | data = s.recv(BUFFER) 39 | s.close() 40 | return True 41 | 42 | def set_crossfade(self,seconds): 43 | return self.__runcmd("crossfade",False,[str(seconds)]) 44 | 45 | def get_crossfade(self): 46 | cfade = self.__runcmd("crossfade",True) 47 | if(cfade): 48 | cfade = int(cfade[11:]) 49 | return cfade 50 | 51 | def current(self): 52 | return self.__runcmd("current",True) 53 | 54 | def crop(self): 55 | return self.__runcmd("crop") 56 | 57 | def clear(self): 58 | return self.__runcmd("clear") 59 | 60 | def pause(self): 61 | return self.__runcmd("pause") 62 | 63 | def delete(self,songid=0): 64 | return self.__runcmd("del",False,[songid]) 65 | 66 | def idle(self,filter=None): 67 | if filter != None: 68 | return self.__runcmd("idle",True,[filter]) 69 | else: 70 | return self.__runcmd("idle",True) 71 | 72 | def play(self,songid=None): 73 | if(songid != None): 74 | return self.__runcmd("play",False,[songid]) 75 | else: 76 | return self.__runcmd("play",False) 77 | 78 | def listall(self,playlist=None): 79 | if playlist != None: 80 | return self.__runcmd("listall",True,[playlist]).split("\n") 81 | else: 82 | return self.__runcmd("listall",True).split("\n") 83 | 84 | def load(self,playlist): 85 | return self.__runcmd("load",False,[playlist]) 86 | 87 | def ls(self,directory=None): 88 | if directory != None: 89 | return self.__runcmd("ls",True,[directory]) 90 | else: 91 | return self.__runcmd("ls",True) 92 | 93 | def lsplaylists(self,directory=None): 94 | return self.__runcmd("lsplaylists",True) 95 | 96 | def move(self,frm,to): 97 | return self.__runcmd("move",False,[frm,to]) 98 | 99 | def next(self): 100 | return self.__runcmd("next") 101 | 102 | def playlist(self,playlist=None): 103 | if playlist != None: 104 | return self.__runcmd("playlist",True,[playlist]) 105 | else: 106 | return self.__runcmd("playlist",True) 107 | 108 | def prev(self): 109 | return self.__runcmd("prev") 110 | 111 | def random(self,on=None): 112 | if on != None: 113 | return self.__runcmd("random",False,[onoff(om)]) 114 | else: 115 | return self.__runcmd("random",False) 116 | 117 | def repeat(self,on=None): 118 | if on != None: 119 | return self.__runcmd("repeat",False,[onoff(om)]) 120 | else: 121 | return self.__runcmd("repeat",False) 122 | 123 | def single(self,on=None): 124 | if on != None: 125 | return self.__runcmd("single",False,[onoff(om)]) 126 | else: 127 | return self.__runcmd("single",False) 128 | 129 | def consume(self,on=None): 130 | if on != None: 131 | return self.__runcmd("consume",False,[onoff(om)]) 132 | else: 133 | return self.__runcmd("consume",False) 134 | 135 | def rm(self,playlist): 136 | return self.__runcmd("rm",False,[playlist]) 137 | 138 | def save(self,playlist): 139 | return self.__runcmd("save",False,[playlist]) 140 | 141 | def shuffle(self): 142 | return self.__runcmd("shuffle") 143 | 144 | def stats(self): 145 | return self.__runcmd("stats",True) 146 | 147 | def stop(self): 148 | return self.__runcmd("stop") 149 | 150 | def toggle(self): 151 | return self.__runcmd("toggle") 152 | 153 | def add(self,file): 154 | file = '"'+file+'"' 155 | return self.__runtcp("add",False,[file]) 156 | 157 | def insert(self,file): 158 | file = shlex.quote(file) 159 | return self.__runtcp("insert",False,[file]) 160 | 161 | def update(self,wait=False,path=None): 162 | path = shlex.quote(path) 163 | args = [] 164 | 165 | if wait: 166 | args.append("--wait") 167 | 168 | if path != None: 169 | args.append(path) 170 | 171 | return self.__runcmd("update",False,args) 172 | 173 | def version(self): 174 | return self.__runcmd("version") 175 | --------------------------------------------------------------------------------