Repository: ZHANGTIANYAO1/TS3AudioBot-NetEaseCloudmusic-plugin
Branch: main
Commit: 178a7708f859
Files: 507
Total size: 3.9 MB
Directory structure:
gitextract_1muog4nl/
├── ClassLibrary4.csproj
├── ClassLibrary4.sln
├── LICENSE
├── README.md
├── TS3AudioBot/
│ ├── Algorithm/
│ │ ├── IFilterAlgorithm.cs
│ │ ├── LruCache.cs
│ │ └── TimedCache.cs
│ ├── Audio/
│ │ ├── AudioValues.cs
│ │ ├── CustomTargetPipe.cs
│ │ ├── FfmpegProducer.cs
│ │ ├── IPlayerSource.cs
│ │ ├── IVoiceTarget.cs
│ │ ├── PlayInfo.cs
│ │ ├── PlayInfoEventArgs.cs
│ │ ├── PlayManager.cs
│ │ ├── Player.cs
│ │ ├── SongEndEventArgs.cs
│ │ ├── SongInfoChanged.cs
│ │ ├── StallCheckPipe.cs
│ │ └── StreamAudioPlayerSource.cs
│ ├── Bot.cs
│ ├── BotManager.cs
│ ├── CallerInfo.cs
│ ├── ClientCall.cs
│ ├── CommandSystem/
│ │ ├── Ast/
│ │ │ ├── AstCommand.cs
│ │ │ ├── AstError.cs
│ │ │ ├── AstNode.cs
│ │ │ ├── AstType.cs
│ │ │ ├── AstValue.cs
│ │ │ └── StringType.cs
│ │ ├── BotCommand.cs
│ │ ├── CommandAttribute.cs
│ │ ├── CommandException.cs
│ │ ├── CommandManager.cs
│ │ ├── CommandParser.cs
│ │ ├── CommandResults/
│ │ │ ├── IAudioResourceResult.cs
│ │ │ ├── IWrappedResult.cs
│ │ │ ├── PickObjectCommand.cs
│ │ │ └── TailString.cs
│ │ ├── CommandSystemExtensions.cs
│ │ ├── CommandSystemTypes.cs
│ │ ├── Commands/
│ │ │ ├── AliasCommand.cs
│ │ │ ├── AppliedCommand.cs
│ │ │ ├── CommandGroup.cs
│ │ │ ├── FunctionCommand.cs
│ │ │ ├── ICommand.cs
│ │ │ ├── LazyCommand.cs
│ │ │ ├── OverloadedFunctionCommand.cs
│ │ │ ├── ResultCommand.cs
│ │ │ └── RootCommand.cs
│ │ ├── ExecutionInformation.cs
│ │ ├── ICommandBag.cs
│ │ ├── StaticList.cs
│ │ └── Text/
│ │ ├── AppliedTextMod.cs
│ │ ├── Color.cs
│ │ ├── LongTextBehaviour.cs
│ │ ├── LongTextTransform.cs
│ │ ├── TextMod.cs
│ │ ├── TextModBuilder.cs
│ │ ├── TextModFlag.cs
│ │ └── TextModHelper.cs
│ ├── Config/
│ │ ├── Config.cs
│ │ ├── ConfigArray.cs
│ │ ├── ConfigDynamicTable.cs
│ │ ├── ConfigEnumerable.cs
│ │ ├── ConfigHelper.cs
│ │ ├── ConfigPart.cs
│ │ ├── ConfigStructs.cs
│ │ ├── ConfigTable.cs
│ │ ├── ConfigUpgrade2.cs
│ │ └── ConfigValue.cs
│ ├── Core.cs
│ ├── DbStore.cs
│ ├── Dependency/
│ │ ├── BasicInjector.cs
│ │ ├── ChainedInjector.cs
│ │ ├── DependencyBuilder.cs
│ │ ├── IInjector.cs
│ │ ├── InjectorExtensions.cs
│ │ ├── Module.cs
│ │ └── NullInjector.cs
│ ├── Environment/
│ │ ├── Stats.cs
│ │ ├── SystemData.cs
│ │ └── SystemMonitor.cs
│ ├── Error.cs
│ ├── Helper/
│ │ ├── AttributeStrings.cs
│ │ ├── Const.cs
│ │ ├── Diagnose/
│ │ │ ├── SelfDiagnoseLevel.cs
│ │ │ └── SelfDiagnoseMessage.cs
│ │ ├── IJsonConfig.cs
│ │ ├── ImageUtil.cs
│ │ ├── Interactive.cs
│ │ ├── LimitStream.cs
│ │ ├── TextUtil.cs
│ │ ├── TomlTools.cs
│ │ ├── Util.cs
│ │ └── WebWrapper.cs
│ ├── History/
│ │ ├── AudioLogEntry.cs
│ │ ├── HistoryManager.cs
│ │ ├── HistorySaveData.cs
│ │ ├── IHistoryFormatter.cs
│ │ ├── SearchQuery.cs
│ │ └── SmartHistoryFormatter.cs
│ ├── InvokerData.cs
│ ├── Limits.cs
│ ├── Localization/
│ │ ├── DynamicResourceManager.cs
│ │ ├── LocalStr.cs
│ │ ├── LocalizationManager.cs
│ │ ├── strings.Designer.cs
│ │ └── strings.resx
│ ├── MainCommands.cs
│ ├── Playlists/
│ │ ├── LoopMode.cs
│ │ ├── Parser/
│ │ │ └── JspfContent.cs
│ │ ├── Playlist.cs
│ │ ├── PlaylistApiExtensions.cs
│ │ ├── PlaylistIO.cs
│ │ ├── PlaylistItem.cs
│ │ ├── PlaylistManager.cs
│ │ └── Shuffle/
│ │ ├── IShuffleAlgorithm.cs
│ │ ├── LinearFeedbackShiftRegister.cs
│ │ ├── ListedShuffle.cs
│ │ └── NormalOrder.cs
│ ├── Plugins/
│ │ ├── ITabPlugin.cs
│ │ ├── Plugin.cs
│ │ ├── PluginCommandBag.cs
│ │ ├── PluginExtensions.cs
│ │ ├── PluginManager.cs
│ │ ├── PluginObjects.cs
│ │ ├── PluginResponse.cs
│ │ └── PluginStatus.cs
│ ├── Properties/
│ │ └── PublishProfiles/
│ │ ├── FolderProfile.pubxml
│ │ └── FolderProfile.pubxml.user
│ ├── Properties.cs
│ ├── ResourceFactories/
│ │ ├── AudioResource.cs
│ │ ├── AudioTags/
│ │ │ ├── AudioTagReader.cs
│ │ │ ├── BinaryReaderBigEndianExtensions.cs
│ │ │ └── M3uReader.cs
│ │ ├── BandcampResolver.cs
│ │ ├── IPlaylistResolver.cs
│ │ ├── IResolver.cs
│ │ ├── IResourceResolver.cs
│ │ ├── ISearchResolver.cs
│ │ ├── IThumbnailResolver.cs
│ │ ├── MatchCertainty.cs
│ │ ├── MediaResolver.cs
│ │ ├── PlayResource.cs
│ │ ├── ResolveContext.cs
│ │ ├── ResourceResolver.cs
│ │ ├── SongInfo.cs
│ │ ├── SoundcloudResolver.cs
│ │ ├── TwitchResolver.cs
│ │ ├── Youtube/
│ │ │ ├── Json.cs
│ │ │ ├── LoaderPriority.cs
│ │ │ ├── VideoCodec.cs
│ │ │ ├── VideoData.cs
│ │ │ └── YoutubeResolver.cs
│ │ └── YoutubeDlHelper.cs
│ ├── Resources/
│ │ ├── DefaultRights.toml
│ │ └── NLog.config
│ ├── Rights/
│ │ ├── CreateFileSettings.cs
│ │ ├── ExecuteContext.cs
│ │ ├── Matchers/
│ │ │ ├── MatchApiCallerIp.cs
│ │ │ ├── MatchBot.cs
│ │ │ ├── MatchChannelGroupId.cs
│ │ │ ├── MatchClientGroupId.cs
│ │ │ ├── MatchClientUid.cs
│ │ │ ├── MatchHost.cs
│ │ │ ├── MatchIsApi.cs
│ │ │ ├── MatchPermission.cs
│ │ │ ├── MatchToken.cs
│ │ │ ├── MatchVisibility.cs
│ │ │ ├── Matcher.cs
│ │ │ └── PermCompare.cs
│ │ ├── ParseContext.cs
│ │ ├── RightsDecl.cs
│ │ ├── RightsGroup.cs
│ │ ├── RightsManager.cs
│ │ └── RightsRule.cs
│ ├── Sessions/
│ │ ├── AnonymousSession.cs
│ │ ├── ApiToken.cs
│ │ ├── SessionManager.cs
│ │ ├── TokenManager.cs
│ │ └── UserSession.cs
│ ├── Setup.cs
│ ├── TS3AudioBot.csproj
│ ├── TS3AudioBot.csproj.user
│ ├── Ts3Client.cs
│ ├── Upgrader.cs
│ ├── Web/
│ │ ├── Api/
│ │ │ ├── ApiCall.cs
│ │ │ ├── DataStream.cs
│ │ │ ├── JsonArray.cs
│ │ │ ├── JsonEmpty.cs
│ │ │ ├── JsonError.cs
│ │ │ ├── JsonObject.cs
│ │ │ ├── JsonValue.cs
│ │ │ ├── OpenApiGenerator.cs
│ │ │ ├── TimeSpanConverter.cs
│ │ │ └── WebApi.cs
│ │ ├── Model/
│ │ │ ├── CurrentSongInfo.cs
│ │ │ ├── PlaylistInfo.cs
│ │ │ ├── PlaylistItemGetData.cs
│ │ │ └── QueueInfo.cs
│ │ └── WebServer.cs
│ ├── build.csx
│ └── obj/
│ ├── Debug/
│ │ ├── TS3AudioBot.1.0.0.nuspec
│ │ ├── net7.0/
│ │ │ └── TS3AudioBot.csproj.AssemblyReference.cache
│ │ ├── net7.0-windows/
│ │ │ ├── .NETCoreApp,Version=v7.0.AssemblyAttributes.cs
│ │ │ ├── TS3AudioBot.AssemblyInfo.cs
│ │ │ ├── TS3AudioBot.AssemblyInfoInputs.cache
│ │ │ ├── TS3AudioBot.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TS3AudioBot.Localization.strings.resources
│ │ │ ├── TS3AudioBot.assets.cache
│ │ │ ├── TS3AudioBot.csproj.AssemblyReference.cache
│ │ │ ├── TS3AudioBot.csproj.CopyComplete
│ │ │ ├── TS3AudioBot.csproj.CoreCompileInputs.cache
│ │ │ ├── TS3AudioBot.csproj.FileListAbsolute.txt
│ │ │ ├── TS3AudioBot.csproj.GenerateResource.cache
│ │ │ ├── TS3AudioBot.csproj.SuggestedBindingRedirects.cache
│ │ │ ├── TS3AudioBot.dll.config
│ │ │ ├── TS3AudioBot.genruntimeconfig.cache
│ │ │ └── TS3AudioBot.pdb
│ │ └── netcoreapp3.1/
│ │ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ │ ├── TS3AudioBot.AssemblyInfo.cs
│ │ ├── TS3AudioBot.AssemblyInfoInputs.cache
│ │ ├── TS3AudioBot.GeneratedMSBuildEditorConfig.editorconfig
│ │ ├── TS3AudioBot.Localization.strings.resources
│ │ ├── TS3AudioBot.assets.cache
│ │ ├── TS3AudioBot.csproj.AssemblyReference.cache
│ │ ├── TS3AudioBot.csproj.CopyComplete
│ │ ├── TS3AudioBot.csproj.CoreCompileInputs.cache
│ │ ├── TS3AudioBot.csproj.FileListAbsolute.txt
│ │ ├── TS3AudioBot.csproj.GenerateResource.cache
│ │ ├── TS3AudioBot.csproj.SuggestedBindingRedirects.cache
│ │ ├── TS3AudioBot.dll.config
│ │ ├── TS3AudioBot.genruntimeconfig.cache
│ │ └── TS3AudioBot.pdb
│ ├── Release/
│ │ ├── net7.0-windows/
│ │ │ ├── .NETCoreApp,Version=v7.0.AssemblyAttributes.cs
│ │ │ ├── TS3AudioBot.AssemblyInfo.cs
│ │ │ ├── TS3AudioBot.AssemblyInfoInputs.cache
│ │ │ ├── TS3AudioBot.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TS3AudioBot.assets.cache
│ │ │ └── TS3AudioBot.csproj.AssemblyReference.cache
│ │ └── netcoreapp3.1/
│ │ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ │ ├── PublishOutputs.25bc18e9a6.txt
│ │ ├── TS3AudioBot.AssemblyInfo.cs
│ │ ├── TS3AudioBot.AssemblyInfoInputs.cache
│ │ ├── TS3AudioBot.GeneratedMSBuildEditorConfig.editorconfig
│ │ ├── TS3AudioBot.Localization.strings.resources
│ │ ├── TS3AudioBot.assets.cache
│ │ ├── TS3AudioBot.csproj.AssemblyReference.cache
│ │ ├── TS3AudioBot.csproj.CopyComplete
│ │ ├── TS3AudioBot.csproj.CoreCompileInputs.cache
│ │ ├── TS3AudioBot.csproj.FileListAbsolute.txt
│ │ ├── TS3AudioBot.csproj.GenerateResource.cache
│ │ ├── TS3AudioBot.csproj.SuggestedBindingRedirects.cache
│ │ ├── TS3AudioBot.dll.config
│ │ ├── TS3AudioBot.genruntimeconfig.cache
│ │ ├── TS3AudioBot.pdb
│ │ ├── linux-x64/
│ │ │ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ │ │ ├── PublishOutputs.c54adf16a2.txt
│ │ │ ├── TS3AudioBot.AssemblyInfo.cs
│ │ │ ├── TS3AudioBot.AssemblyInfoInputs.cache
│ │ │ ├── TS3AudioBot.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TS3AudioBot.Localization.strings.resources
│ │ │ ├── TS3AudioBot.assets.cache
│ │ │ ├── TS3AudioBot.csproj.AssemblyReference.cache
│ │ │ ├── TS3AudioBot.csproj.CopyComplete
│ │ │ ├── TS3AudioBot.csproj.CoreCompileInputs.cache
│ │ │ ├── TS3AudioBot.csproj.FileListAbsolute.txt
│ │ │ ├── TS3AudioBot.csproj.GenerateResource.cache
│ │ │ ├── TS3AudioBot.csproj.SuggestedBindingRedirects.cache
│ │ │ ├── TS3AudioBot.deps.json
│ │ │ ├── TS3AudioBot.dll.config
│ │ │ ├── TS3AudioBot.genruntimeconfig.cache
│ │ │ ├── TS3AudioBot.pdb
│ │ │ └── apphost
│ │ └── win-x64/
│ │ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ │ ├── PublishOutputs.cf05aea114.txt
│ │ ├── TS3AudioBot.AssemblyInfo.cs
│ │ ├── TS3AudioBot.AssemblyInfoInputs.cache
│ │ ├── TS3AudioBot.GeneratedMSBuildEditorConfig.editorconfig
│ │ ├── TS3AudioBot.Localization.strings.resources
│ │ ├── TS3AudioBot.assets.cache
│ │ ├── TS3AudioBot.csproj.AssemblyReference.cache
│ │ ├── TS3AudioBot.csproj.CopyComplete
│ │ ├── TS3AudioBot.csproj.CoreCompileInputs.cache
│ │ ├── TS3AudioBot.csproj.FileListAbsolute.txt
│ │ ├── TS3AudioBot.csproj.GenerateResource.cache
│ │ ├── TS3AudioBot.csproj.SuggestedBindingRedirects.cache
│ │ ├── TS3AudioBot.deps.json
│ │ ├── TS3AudioBot.dll.config
│ │ ├── TS3AudioBot.genruntimeconfig.cache
│ │ └── TS3AudioBot.pdb
│ ├── TS3AudioBot.csproj.nuget.dgspec.json
│ ├── TS3AudioBot.csproj.nuget.g.props
│ ├── TS3AudioBot.csproj.nuget.g.targets
│ ├── project.assets.json
│ ├── project.nuget.cache
│ └── publish/
│ ├── linux-x64/
│ │ ├── TS3AudioBot.csproj.nuget.dgspec.json
│ │ ├── TS3AudioBot.csproj.nuget.g.props
│ │ ├── TS3AudioBot.csproj.nuget.g.targets
│ │ ├── project.assets.json
│ │ └── project.nuget.cache
│ └── win-x64/
│ ├── TS3AudioBot.csproj.nuget.dgspec.json
│ ├── TS3AudioBot.csproj.nuget.g.props
│ ├── TS3AudioBot.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
├── TSLib/
│ ├── Audio/
│ │ ├── AudioInterfaces.cs
│ │ ├── AudioMeta.cs
│ │ ├── AudioPacketReader.cs
│ │ ├── AudioPipeExtensions.cs
│ │ ├── AudioTools.cs
│ │ ├── CheckActivePipe.cs
│ │ ├── ClientMixdown.cs
│ │ ├── DecoderPipe.cs
│ │ ├── EncoderPipe.cs
│ │ ├── Opus/
│ │ │ ├── LICENSE
│ │ │ ├── NativeMethods.cs
│ │ │ ├── OPUS_LICENSE
│ │ │ ├── OpusDecoder.cs
│ │ │ ├── OpusEncoder.cs
│ │ │ └── README
│ │ ├── PassiveMergePipe.cs
│ │ ├── PassiveSplitterPipe.cs
│ │ ├── PreciseAudioTimer.cs
│ │ ├── PreciseTimedPipe.cs
│ │ ├── StaticMetaPipe.cs
│ │ ├── StreamAudioProducer.cs
│ │ └── VolumePipe.cs
│ ├── Commands/
│ │ ├── CommandMultiParameter.cs
│ │ ├── CommandOption.cs
│ │ ├── CommandParameter.cs
│ │ ├── ICommandPart.cs
│ │ ├── TsCommand.cs
│ │ ├── TsCommand.gen.cs
│ │ ├── TsCommand.gen.tt
│ │ ├── TsConst.cs
│ │ └── TsString.cs
│ ├── ConnectionData.cs
│ ├── DisconnectEventArgs.cs
│ ├── EventDispatcher.cs
│ ├── Full/
│ │ ├── Book/
│ │ │ ├── Book.cs
│ │ │ └── SpecialTypes.cs
│ │ ├── GenerationWindow.cs
│ │ ├── IdentityData.cs
│ │ ├── License.cs
│ │ ├── NetworkStats.cs
│ │ ├── Packet.cs
│ │ ├── PacketHandler.cs
│ │ ├── PacketType.cs
│ │ ├── QuickerLz.cs
│ │ ├── RingQueue.cs
│ │ ├── TsCrypt.cs
│ │ ├── TsFullClient.cs
│ │ ├── TsFullClient.gen.cs
│ │ └── TsFullClient.gen.tt
│ ├── Generated/
│ │ ├── Book.cs
│ │ ├── Book.tt
│ │ ├── BookParser.ttinclude
│ │ ├── ErrorParser.ttinclude
│ │ ├── M2B.cs
│ │ ├── M2B.tt
│ │ ├── M2BParser.ttinclude
│ │ ├── MessageParser.ttinclude
│ │ ├── Messages.cs
│ │ ├── Messages.tt
│ │ ├── NotificationUtil.ttinclude
│ │ ├── TsErrorCode.cs
│ │ ├── TsErrorCode.tt
│ │ ├── TsPermission.cs
│ │ ├── TsPermission.tt
│ │ ├── TsVersion.gen.cs
│ │ ├── TsVersion.gen.tt
│ │ └── Util.ttinclude
│ ├── Helper/
│ │ ├── AsyncEventHandler.cs
│ │ ├── CommandErrorExtensions.cs
│ │ ├── DebugUtil.cs
│ │ ├── LogId.cs
│ │ ├── MissingEnumCaseException.cs
│ │ ├── NativeLibraryLoader.cs
│ │ ├── R.cs
│ │ ├── SpanExtensions.cs
│ │ ├── SpanSplitter.cs
│ │ └── Tools.cs
│ ├── LazyNotification.cs
│ ├── MessageProcessor.cs
│ ├── Messages/
│ │ ├── BaseTypes.cs
│ │ ├── Deserializer.cs
│ │ ├── MessageAdditions.cs
│ │ ├── PermissionTransform.cs
│ │ └── ResponseDictionary.cs
│ ├── Properties.cs
│ ├── Query/
│ │ ├── TsQueryClient.cs
│ │ ├── TsQueryClient.gen.cs
│ │ └── TsQueryClient.gen.tt
│ ├── Scheduler/
│ │ ├── DedicatedTaskScheduler.cs
│ │ ├── DispatcherHelper.cs
│ │ └── TickWorker.cs
│ ├── TSLib.csproj
│ ├── TsBaseFunctions.FileTransfer.cs
│ ├── TsBaseFunctions.cs
│ ├── TsBaseFunctions.gen.cs
│ ├── TsBaseFunctions.gen.tt
│ ├── TsDnsResolver.cs
│ ├── TsEnums.cs
│ ├── TsPermissionHelper.cs
│ ├── TsVersion.cs
│ ├── Types.cs
│ ├── Types.gen.cs
│ ├── Types.gen.tt
│ ├── WaitBlock.cs
│ ├── bin/
│ │ ├── Debug/
│ │ │ ├── netcoreapp3.1/
│ │ │ │ ├── TSLib.deps.json
│ │ │ │ └── TSLib.pdb
│ │ │ ├── netstandard2.0/
│ │ │ │ ├── TSLib.deps.json
│ │ │ │ └── TSLib.pdb
│ │ │ └── netstandard2.1/
│ │ │ ├── TSLib.deps.json
│ │ │ └── TSLib.pdb
│ │ └── Release/
│ │ ├── netcoreapp3.1/
│ │ │ ├── TSLib.deps.json
│ │ │ └── TSLib.pdb
│ │ ├── netstandard2.0/
│ │ │ ├── TSLib.deps.json
│ │ │ └── TSLib.pdb
│ │ └── netstandard2.1/
│ │ ├── TSLib.deps.json
│ │ └── TSLib.pdb
│ ├── dnc2_compat/
│ │ ├── Extensions.cs
│ │ ├── Range.cs
│ │ └── info.txt
│ └── obj/
│ ├── Debug/
│ │ ├── netcoreapp3.1/
│ │ │ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ │ │ ├── TSLib.AssemblyInfo.cs
│ │ │ ├── TSLib.AssemblyInfoInputs.cache
│ │ │ ├── TSLib.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TSLib.assets.cache
│ │ │ ├── TSLib.csproj.AssemblyReference.cache
│ │ │ ├── TSLib.csproj.CoreCompileInputs.cache
│ │ │ ├── TSLib.csproj.FileListAbsolute.txt
│ │ │ └── TSLib.pdb
│ │ ├── netstandard2.0/
│ │ │ ├── .NETStandard,Version=v2.0.AssemblyAttributes.cs
│ │ │ ├── NuGet/
│ │ │ │ ├── 2C8E6E8C03FF0327F78E3CB90559803756F36314/
│ │ │ │ │ └── Nullable/
│ │ │ │ │ └── 1.2.1/
│ │ │ │ │ └── Nullable/
│ │ │ │ │ └── NullableAttributes.cs
│ │ │ │ └── 7BA94E4E53727142735FA3B08F79617CD03664FD/
│ │ │ │ └── Nullable/
│ │ │ │ └── 1.2.1/
│ │ │ │ └── Nullable/
│ │ │ │ └── NullableAttributes.cs
│ │ │ ├── TSLib.AssemblyInfo.cs
│ │ │ ├── TSLib.AssemblyInfoInputs.cache
│ │ │ ├── TSLib.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TSLib.assets.cache
│ │ │ ├── TSLib.csproj.AssemblyReference.cache
│ │ │ ├── TSLib.csproj.CoreCompileInputs.cache
│ │ │ ├── TSLib.csproj.FileListAbsolute.txt
│ │ │ └── TSLib.pdb
│ │ └── netstandard2.1/
│ │ ├── .NETStandard,Version=v2.1.AssemblyAttributes.cs
│ │ ├── TSLib.AssemblyInfo.cs
│ │ ├── TSLib.AssemblyInfoInputs.cache
│ │ ├── TSLib.GeneratedMSBuildEditorConfig.editorconfig
│ │ ├── TSLib.assets.cache
│ │ ├── TSLib.csproj.AssemblyReference.cache
│ │ ├── TSLib.csproj.CoreCompileInputs.cache
│ │ ├── TSLib.csproj.FileListAbsolute.txt
│ │ └── TSLib.pdb
│ ├── Release/
│ │ ├── netcoreapp3.1/
│ │ │ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ │ │ ├── TSLib.AssemblyInfo.cs
│ │ │ ├── TSLib.AssemblyInfoInputs.cache
│ │ │ ├── TSLib.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TSLib.assets.cache
│ │ │ ├── TSLib.csproj.AssemblyReference.cache
│ │ │ ├── TSLib.csproj.CoreCompileInputs.cache
│ │ │ ├── TSLib.csproj.FileListAbsolute.txt
│ │ │ └── TSLib.pdb
│ │ ├── netstandard2.0/
│ │ │ ├── .NETStandard,Version=v2.0.AssemblyAttributes.cs
│ │ │ ├── NuGet/
│ │ │ │ ├── 2C8E6E8C03FF0327F78E3CB90559803756F36314/
│ │ │ │ │ └── Nullable/
│ │ │ │ │ └── 1.2.1/
│ │ │ │ │ └── Nullable/
│ │ │ │ │ └── NullableAttributes.cs
│ │ │ │ └── 7BA94E4E53727142735FA3B08F79617CD03664FD/
│ │ │ │ └── Nullable/
│ │ │ │ └── 1.2.1/
│ │ │ │ └── Nullable/
│ │ │ │ └── NullableAttributes.cs
│ │ │ ├── TSLib.AssemblyInfo.cs
│ │ │ ├── TSLib.AssemblyInfoInputs.cache
│ │ │ ├── TSLib.GeneratedMSBuildEditorConfig.editorconfig
│ │ │ ├── TSLib.assets.cache
│ │ │ ├── TSLib.csproj.AssemblyReference.cache
│ │ │ ├── TSLib.csproj.CoreCompileInputs.cache
│ │ │ ├── TSLib.csproj.FileListAbsolute.txt
│ │ │ └── TSLib.pdb
│ │ └── netstandard2.1/
│ │ ├── .NETStandard,Version=v2.1.AssemblyAttributes.cs
│ │ ├── TSLib.AssemblyInfo.cs
│ │ ├── TSLib.AssemblyInfoInputs.cache
│ │ ├── TSLib.GeneratedMSBuildEditorConfig.editorconfig
│ │ ├── TSLib.assets.cache
│ │ ├── TSLib.csproj.AssemblyReference.cache
│ │ ├── TSLib.csproj.CoreCompileInputs.cache
│ │ ├── TSLib.csproj.FileListAbsolute.txt
│ │ └── TSLib.pdb
│ ├── TSLib.csproj.nuget.dgspec.json
│ ├── TSLib.csproj.nuget.g.props
│ ├── TSLib.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
├── YunBot.cs
└── obj/
├── ClassLibrary4.csproj.nuget.dgspec.json
├── ClassLibrary4.csproj.nuget.g.props
├── ClassLibrary4.csproj.nuget.g.targets
├── Debug/
│ ├── netcoreapp3.0/
│ │ ├── ClassLibrary4.assets.cache
│ │ └── ClassLibrary4.csproj.FileListAbsolute.txt
│ └── netcoreapp3.1/
│ ├── .NETCoreApp,Version=v3.1.AssemblyAttributes.cs
│ ├── ClassLibrary4.AssemblyInfo.cs
│ ├── ClassLibrary4.AssemblyInfoInputs.cache
│ ├── ClassLibrary4.GeneratedMSBuildEditorConfig.editorconfig
│ ├── ClassLibrary4.assets.cache
│ ├── ClassLibrary4.csproj.AssemblyReference.cache
│ ├── ClassLibrary4.csproj.CopyComplete
│ ├── ClassLibrary4.csproj.CoreCompileInputs.cache
│ ├── ClassLibrary4.csproj.FileListAbsolute.txt
│ └── YunBot.pdb
├── project.assets.json
└── project.nuget.cache
================================================
FILE CONTENTS
================================================
================================================
FILE: ClassLibrary4.csproj
================================================
netcoreapp3.1
YunBot
================================================
FILE: ClassLibrary4.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.33927.289
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary4", "ClassLibrary4.csproj", "{D3D9037A-AD7B-4C9E-9FC0-CA330D8776BD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TSLib", "G:\TS3DEV\TS3AudioBot-master\TSLib\TSLib.csproj", "{66AF7F2B-43B7-41E0-982A-FCD7DA062BAA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TS3AudioBot", "G:\TS3DEV\TS3AudioBot-master\TS3AudioBot\TS3AudioBot.csproj", "{FFC15959-0725-4B2B-BF06-BFE8403C60A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D3D9037A-AD7B-4C9E-9FC0-CA330D8776BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D3D9037A-AD7B-4C9E-9FC0-CA330D8776BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3D9037A-AD7B-4C9E-9FC0-CA330D8776BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3D9037A-AD7B-4C9E-9FC0-CA330D8776BD}.Release|Any CPU.Build.0 = Release|Any CPU
{66AF7F2B-43B7-41E0-982A-FCD7DA062BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66AF7F2B-43B7-41E0-982A-FCD7DA062BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66AF7F2B-43B7-41E0-982A-FCD7DA062BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66AF7F2B-43B7-41E0-982A-FCD7DA062BAA}.Release|Any CPU.Build.0 = Release|Any CPU
{FFC15959-0725-4B2B-BF06-BFE8403C60A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FFC15959-0725-4B2B-BF06-BFE8403C60A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFC15959-0725-4B2B-BF06-BFE8403C60A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFC15959-0725-4B2B-BF06-BFE8403C60A6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {86AB0697-D2EC-474E-8C8F-67F25BEDB10E}
EndGlobalSection
EndGlobal
================================================
FILE: LICENSE
================================================
Open Software License ("OSL") v 3.0
This Open Software License (the "License") applies to any original work of
authorship (the "Original Work") whose owner (the "Licensor") has placed the
following licensing notice adjacent to the copyright notice for the Original
Work:
Licensed under the Open Software License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free,
non-exclusive, sublicensable license, for the duration of the copyright, to do
the following:
a) to reproduce the Original Work in copies, either alone or as part of a
collective work;
b) to translate, adapt, alter, transform, modify, or arrange the Original
Work, thereby creating derivative works ("Derivative Works") based upon the
Original Work;
c) to distribute or communicate copies of the Original Work and Derivative
Works to the public, with the proviso that copies of Original Work or
Derivative Works that You distribute or communicate shall be licensed under
this Open Software License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free,
non-exclusive, sublicensable license, under patent claims owned or controlled
by the Licensor that are embodied in the Original Work as furnished by the
Licensor, for the duration of the patents, to make, use, sell, offer for sale,
have made, and import the Original Work and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the preferred
form of the Original Work for making modifications to it and all available
documentation describing how to modify the Original Work. Licensor agrees to
provide a machine-readable copy of the Source Code of the Original Work along
with each copy of the Original Work that Licensor distributes. Licensor
reserves the right to satisfy this obligation by placing a machine-readable
copy of the Source Code in an information repository reasonably calculated to
permit inexpensive and convenient access by You for as long as Licensor
continues to distribute the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the names
of any contributors to the Original Work, nor any of their trademarks or
service marks, may be used to endorse or promote products derived from this
Original Work without express prior permission of the Licensor. Except as
expressly stated herein, nothing in this License grants any license to
Licensor's trademarks, copyrights, patents, trade secrets or any other
intellectual property. No patent license is granted to make, use, sell, offer
for sale, have made, or import embodiments of any patent claims other than the
licensed claims defined in Section 2. No license is granted to the trademarks
of Licensor even if such marks are included in the Original Work. Nothing in
this License shall be interpreted to prohibit Licensor from licensing under
terms different from this License any Original Work that Licensor otherwise
would have a right to license.
5) External Deployment. The term "External Deployment" means the use,
distribution, or communication of the Original Work or Derivative Works in any
way such that the Original Work or Derivative Works may be used by anyone
other than You, whether those works are distributed or communicated to those
persons or made available as an application intended for use over a network.
As an express condition for the grants of license hereunder, You must treat
any External Deployment by You of the Original Work or a Derivative Work as a
distribution under section 1(c).
6) Attribution Rights. You must retain, in the Source Code of any Derivative
Works that You create, all copyright, patent, or trademark notices from the
Source Code of the Original Work, as well as any notices of licensing and any
descriptive text identified therein as an "Attribution Notice." You must cause
the Source Code for any Derivative Works that You create to carry a prominent
Attribution Notice reasonably calculated to inform recipients that You have
modified the Original Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
the copyright in and to the Original Work and the patent rights granted herein
by Licensor are owned by the Licensor or are sublicensed to You under the
terms of this License with the permission of the contributor(s) of those
copyrights and patent rights. Except as expressly stated in the immediately
preceding sentence, the Original Work is provided under this License on an "AS
IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without
limitation, the warranties of non-infringement, merchantability or fitness for
a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK
IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this
License. No license to the Original Work is granted by this License except
under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal theory,
whether in tort (including negligence), contract, or otherwise, shall the
Licensor be liable to anyone for any indirect, special, incidental, or
consequential damages of any character arising as a result of this License or
the use of the Original Work including, without limitation, damages for loss
of goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses. This limitation of liability shall not
apply to the extent applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly assented to this
License, that assent indicates your clear and irrevocable acceptance of this
License and all of its terms and conditions. If You distribute or communicate
copies of the Original Work or a Derivative Work, You must make a reasonable
effort under the circumstances to obtain the express assent of recipients to
the terms of this License. This License conditions your rights to undertake
the activities listed in Section 1, including your right to create Derivative
Works based upon the Original Work, and doing so without honoring these terms
and conditions is prohibited by copyright law and international treaty.
Nothing in this License is intended to affect copyright exceptions and
limitations (including "fair use" or "fair dealing"). This License shall
terminate immediately and You may no longer exercise any of the rights granted
to You by this License upon your failure to honor the conditions in Section
1(c).
10) Termination for Patent Action. This License shall terminate automatically
and You may no longer exercise any of the rights granted to You by this
License as of the date You commence an action, including a cross-claim or
counterclaim, against Licensor or any licensee alleging that the Original Work
infringes a patent. This termination provision shall not apply for an action
alleging patent infringement by combinations of the Original Work with other
software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
License may be brought only in the courts of a jurisdiction wherein the
Licensor resides or in which Licensor conducts its primary business, and under
the laws of that jurisdiction excluding its conflict-of-law provisions. The
application of the United Nations Convention on Contracts for the
International Sale of Goods is expressly excluded. Any use of the Original
Work outside the scope of this License or after its termination shall be
subject to the requirements and penalties of copyright or patent law in the
appropriate jurisdiction. This section shall survive the termination of this
License.
12) Attorneys' Fees. In any action to enforce the terms of this License or
seeking damages relating thereto, the prevailing party shall be entitled to
recover its costs and expenses, including, without limitation, reasonable
attorneys' fees and costs incurred in connection with such action, including
any appeal of such action. This section shall survive the termination of this
License.
13) Miscellaneous. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent necessary
to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License,
whether in upper or lower case, means an individual or a legal entity
exercising rights under, and complying with all of the terms of, this License.
For legal entities, "You" includes any entity that controls, is controlled by,
or is under common control with you. For purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the direction or
management of such entity, whether by contract or otherwise, or (ii) ownership
of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not otherwise
restricted or conditioned by this License or by law, and Licensor promises not
to interfere with or be responsible for such uses by You.
16) Modification of This License. This License is Copyright © 2005 Lawrence
Rosen. Permission is granted to copy, distribute, or communicate this License
without modification. Nothing in this License permits You to modify this
License as applied to the Original Work or to Derivative Works. However, You
may modify the text of this License and copy, distribute or communicate your
modified version (the "Modified License") and apply it to other original works
of authorship subject to the following conditions: (i) You may not indicate in
any way that your Modified License is the "Open Software License" or "OSL" and
you may not use those names in the name of your Modified License; (ii) You
must replace the notice specified in the first paragraph above with the notice
"Licensed under " or with a notice of your own
that is not confusingly similar to the notice in this License; and (iii) You
may not claim that your original works are open source software unless your
Modified License has been approved by Open Source Initiative (OSI) and You
comply with its license review and certification process.
================================================
FILE: README.md
================================================
# ⚠️ 本项目已停止更新和维护
**请使用最新的项目:[teamspeak-music-bot](https://github.com/ZHANGTIANYAO1/teamspeak-music-bot)**
本仓库将不再进行任何更新或维护,建议所有用户迁移到新项目。
---
# TS3AudioBot-NetEaseCloudmusic-plugin
>此插件基于Splamy/TS3AudioBot项目 https://github.com/Splamy/TS3AudioBot
>以及网易云音乐 API开发 [https://gitlab.com/Binaryify/neteasecloudmusicapi ](https://www.npmjs.com/package/NeteaseCloudMusicApi)
此插件安装方法同样见TS3AudioBot项目wiki
**2.0版本之后可以不需要本地部署网易云API了,但是强烈建议自行部署防止隐私泄露 **
**最好给音乐机器人超管权限保证能正常更新头像和描述**
这是一个用C#给TS3AudioBot编写网易云插件,让你的TS可以有一个音乐机器人。如果觉得好的话,还请给个星星支持一下
## 关于API和机器人部署
目前网易云可能有什么改动,导致如果使用部署在海外的API将无法播放部分VIP歌曲,已经在ISSUE中添加新的国内公开API
推荐将API和机器人都部署在国内的服务器上
## 关于 DEV 版本
此版本基于 Splamy/TS3AudioBot 项目和 NeteaseCloudMusicApi 开发。DEV 版本由 @577fkj 大佬重构和增强,感谢他的贡献!
由于原开发环境的丢失,无法继续之前兼容 stable 版本的音乐机器人插件的开发。此 DEV 版本是在 @577fkj 的重构和增强版本基础上进一步开发的。
此版本新增了多项功能,包括查看播放列表、验证码登录、无人自动暂停、清除歌单、获取歌单最大长度限制、让机器人前往当前频道、播放专辑功能、私人 FM 模式等。
### 链接
- DEV 版本 GitHub 仓库:https://github.com/ZHANGTIANYAO1/TS3AudioBot-NetEaseCloudmusic-plugin/tree/DEV
- pre-release(测试版)下载链接:https://github.com/ZHANGTIANYAO1/TS3AudioBot-NetEaseCloudmusic-plugin/releases/tag/3.0.0
### 注意事项
- 此版本windows系统需要使用新的机器人程序,已经打包在pre-release中
- 虽然添加了第三方网易云API,但是为了大家的安全考虑,强烈建议自行部署API
- 需要使用最新版本的网易云API:https://gitlab.com/Binaryify/neteasecloudmusicapi
- 此版本在开发中,可能会有bug,遇到bug请提交issue,如果有想要添加的功能也可以提交issue
- 将机器人整合包的音质默认设置为了最高bitrate
- 权限文件修改为所有人有全部权限
- 使用版本过新的linux可能会有困难,比如使用Ubuntu(24,22)版本的需要自己去安装老的lib库,例子:(libicu70_70.1-2_amd64.deb libssl1.1_1.1.1f-1ubuntu2_amd64.deb)
- 推荐使用Ubuntu20
## 关于设置文件YunSettings.ini
`playMode=`是播放模式
`WangYiYunAPI_Address`是网易云API地址,目前默认的是一个大佬的远程API,如果加载速度过慢或者无法访问,请自行部署API并修改API地址。(为了保护你的隐私强烈建议你自行部署API)
`cookies1=`是保存在你本地的身份验证,通过二维码登录获取。(不需要修改)
## 目前的指令:
正在播放的歌单的图片和名称可以点机器人看它的头像和描述
vip音乐想要先登陆才能播放完整版本:(输入指令后扫描机器人头像二维码登陆)
`!yun login`
双击机器人,目前有以下指令(把[xxx]替换成对应信息,**包括中括号**)
1.立即播放网易云音乐
`!yun play [音乐名称]`
2.添加音乐到下一首
`!yun add [音乐名称]`
3.播放网易云音乐歌单(如果提示Error: Nothing to play...重新输入指令解决)
`!yun gedan [歌单名称]`
4.播放网易云音乐歌单id
`!yun gedanid [歌单名称]`
5.立即播放网易云音乐id
`!yun playid [歌单id]`
6.添加指定音乐id到下一首
`!yun add [音乐id]`
7.播放列表中的下一首
`!yun next`
8.修改播放模式
`!yun mode [模式选择数字0-3]`
`0 = 顺序播放`
`1 = 顺序循环`
`2 = 随机播放`
`3 = 随机循环`
需要注意的是如果歌单歌曲过多需要时间加载,期间一定一定不要输入其他指令
### TS频道描述(复制代码到频道描述)
```
[COLOR=#ff5500][B]正在播放的歌单的图片和名称可以点机器人看它的头像和描述[/B][/COLOR]
[COLOR=#aa00ff]机器人现在可以通过歌单播放vip音乐,如果遇到其他问题可以联系Github[/COLOR]
[COLOR=#0055ff]双击机器人,目前有以下指令([I]把[xxx]替换成对应信息,包括中括号[/I])[/COLOR]
1.立即播放网易云音乐
[COLOR=#00aa00]!yun play [音乐名称][/COLOR]
2.添加音乐到下一首
[COLOR=#00aa00]!yun add [音乐名称][/COLOR]
3.播放网易云音乐歌单
[COLOR=#00aa00]!yun gedan [歌单名称][/COLOR]
4.播放网易云音乐歌单id
[COLOR=#00aa00]!yun gedanid [歌单名称][/COLOR]
5.立即播放网易云音乐id
[COLOR=#00aa00]!yun playid [歌单id][/COLOR]
6.添加指定音乐id到下一首
[COLOR=#00aa00]!yun add [音乐id][/COLOR]
7.播放列表中的下一首
[COLOR=#00aa00]!yun next[/COLOR]
8.播放模式选择【0=顺序播放 1=顺序循环 2=随机 3=随即循环】
[COLOR=#00aa00]!yun mode[/COLOR]
9.登陆账户
[COLOR=#00aa00]!yun login[/COLOR]
[COLOR=#aaaaff]如果想要播放会员音乐需要先登陆会员账户,输入上述命令后扫描机器人头像的二维码登陆(只需要一账户登陆一次即可)[/COLOR]
需要注意的是如果歌单歌曲过多需要时间加载(重写后应该只需要几秒),期间[B]一定一定不要[/B]输入其他指令
以下例子加粗的就是音乐或者歌单id
[URL]https://music.163.com/#/my/m/music/playlist?id=[B]2139305008[I][/I][/B][/URL]
```
### 已知问题
使用官方构建的ts3aduiobot会报错,因为官方的编译环境有问题。
解决方法:1.自行构建 2.使用我打包的版本
重复卸载加载插件会出现问题,如果需要重新加载请先重启Bot
Linux Docker无法正常显示description
解决方法:docker版本启用了web管理界面,在里面的Show song in bot description冲突了。关掉就可以正常显示了
#### 写在最后的话
本人完全不会C#,纯粹是自己花两天自学的,所以请各位大佬轻点喷我。
还有很多地方需要完善以及很多功能可以添加,但是本人无法做出任何承诺
同时欢迎各位来修改和完善这个插件
最后的最后请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码。如果决定使用默认的远程API请自行承担任何可能的后续风险。
================================================
FILE: TS3AudioBot/Algorithm/IFilterAlgorithm.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Collections.Generic;
using System.Linq;
namespace TS3AudioBot.Algorithm
{
public interface IFilter
{
IEnumerable> Filter(IEnumerable> list, string filter);
}
public static class Filter
{
public static IFilter DefaultFilter { get; } = Ic3Filter.Instance;
public static IFilter? GetFilterByName(string filter)
{
return filter switch
{
"exact" => ExactFilter.Instance,
"substring" => SubstringFilter.Instance,
"ic3" => Ic3Filter.Instance,
"hamming" => HammingFilter.Instance,
_ => null,
};
}
public static IFilter GetFilterByNameOrDefault(string filter) => GetFilterByName(filter) ?? DefaultFilter;
}
/// Interleaved continuous character chain.
internal sealed class Ic3Filter : IFilter
{
private Ic3Filter() { }
public static IFilter Instance { get; } = new Ic3Filter();
IEnumerable> IFilter.Filter(IEnumerable> list, string filter)
{
// Convert result to list because it can be enumerated multiple times
var possibilities = list.Select(t => (Name: t.Key, t.Value, Index: 0)).ToList();
// Filter matching commands
foreach (var c in filter.ToLowerInvariant())
{
var newPossibilities = (from p in possibilities
let pos = p.Name.ToLowerInvariant().IndexOf(c, p.Index)
where pos != -1
select (p.Name, p.Value, Index: pos + 1)).ToList();
if (newPossibilities.Count > 0)
possibilities = newPossibilities;
}
// Take command with lowest index
int minIndex = possibilities.Min(t => t.Index);
var cmds = possibilities.Where(t => t.Index == minIndex).ToArray();
// Take the smallest command
int minLength = cmds.Min(c => c.Name.Length);
return cmds.Where(c => c.Name.Length == minLength).Select(fi => new KeyValuePair(fi.Name, fi.Value));
}
}
internal sealed class ExactFilter : IFilter
{
private ExactFilter() { }
public static IFilter Instance { get; } = new ExactFilter();
IEnumerable> IFilter.Filter(IEnumerable> list, string filter)
{
return list.Where(x => x.Key == filter);
}
}
internal sealed class HammingFilter : IFilter
{
private HammingFilter() { }
public static IFilter Instance { get; } = new HammingFilter();
IEnumerable> IFilter.Filter(IEnumerable> list, string filter)
{
throw new System.NotImplementedException();
}
}
internal sealed class SubstringFilter : IFilter
{
private SubstringFilter() { }
public static IFilter Instance { get; } = new SubstringFilter();
IEnumerable> IFilter.Filter(IEnumerable> list, string filter)
{
var result = list.Where(x => x.Key.StartsWith(filter));
using var enu = result.GetEnumerator();
if (!enu.MoveNext())
yield break;
yield return enu.Current;
if (enu.Current.Key == filter)
yield break;
while (enu.MoveNext())
yield return enu.Current;
}
}
}
================================================
FILE: TS3AudioBot/Algorithm/LruCache.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace TS3AudioBot.Algorithm
{
public class LruCache where TK : notnull
{
private readonly int maxCapacity;
private readonly Dictionary> cacheDict = new Dictionary>();
private readonly LinkedList<(TK key, TV value)> lruList = new LinkedList<(TK, TV)>();
public LruCache(int capacity)
{
maxCapacity = capacity;
}
public bool TryGetValue(TK key, [MaybeNullWhen(false)] out TV value)
{
if (cacheDict.TryGetValue(key, out var node))
{
Renew(node);
value = node.Value.value;
return true;
}
value = default!;
return false;
}
public void Set(TK key, TV value)
{
if (cacheDict.TryGetValue(key, out var node))
{
Renew(node);
node.Value = (node.Value.key, value);
return;
}
if (cacheDict.Count >= maxCapacity)
RemoveOldest();
node = lruList.AddLast((key, value));
cacheDict.Add(key, node);
}
public bool Remove(TK key) => cacheDict.Remove(key);
private void Renew(LinkedListNode<(TK, TV)> node)
{
lruList.Remove(node);
lruList.AddLast(node);
}
private void RemoveOldest()
{
var node = lruList.First;
if (node is null)
return;
lruList.RemoveFirst();
cacheDict.Remove(node.Value.key);
}
public void Clear()
{
cacheDict.Clear();
lruList.Clear();
}
}
}
================================================
FILE: TS3AudioBot/Algorithm/TimedCache.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using TSLib.Helper;
namespace TS3AudioBot.Algorithm
{
public class TimedCache where TK : notnull
{
public TimeSpan Timeout { get; }
private readonly ConcurrentDictionary cachedData;
public TimedCache() : this(TimeSpan.FromSeconds(3)) { }
public TimedCache(TimeSpan timeout)
{
Timeout = timeout;
cachedData = new ConcurrentDictionary();
}
public bool TryGetValue(TK key, [MaybeNullWhen(false)] out TV value)
{
if (!cachedData.TryGetValue(key, out var data)
|| Tools.Now - Timeout > data.Timestamp)
{
CleanCache();
value = default!;
return false;
}
value = data.Data;
return true;
}
public void Set(TK key, TV value)
{
cachedData[key] = new TimedData { Data = value, Timestamp = Tools.Now };
}
public void Clear()
{
cachedData.Clear();
}
private void CleanCache()
{
var now = Tools.Now - Timeout;
foreach (var item in cachedData.Where(kvp => now > kvp.Value.Timestamp).ToList())
{
cachedData.TryRemove(item.Key, out _);
}
}
private struct TimedData
{
public TV Data;
public DateTime Timestamp;
}
}
}
================================================
FILE: TS3AudioBot/Audio/AudioValues.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using TSLib.Helper;
namespace TS3AudioBot.Audio
{
public static class AudioValues
{
public const float MinVolume = 0;
public const float MaxVolume = 100;
// Reference explanation for the logarithmic scale
// https://www.dr-lex.be/info-stuff/volumecontrols.html#table1
// Adjusted values for 40dB
private const float fact_a = 1e-2f;
private const float fact_b = 4.61512f;
public static float HumanVolumeToFactor(float value)
{
if (value < MinVolume) return 0;
if (value > MaxVolume) return 1;
// Map input values from [MinVolume, MaxVolume] to [0, 1]
value = (value - MinVolume) / (MaxVolume - MinVolume);
// Scale the value logarithmically
return Tools.Clamp((float)(fact_a * Math.Exp(fact_b * value)) - fact_a, 0, 1);
}
public static float FactorToHumanVolume(float value)
{
if (value < 0) return MinVolume;
if (value > 1) return MaxVolume;
// Undo logarithmical scale
value = Tools.Clamp((float)(Math.Log((value + fact_a) / fact_a) / fact_b), 0, 1);
// Map input values from [0, 1] to [MinVolume, MaxVolume]
return (value * (MaxVolume - MinVolume)) + MinVolume;
}
}
}
================================================
FILE: TS3AudioBot/Audio/CustomTargetPipe.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Collections.Generic;
using System.Linq;
using TSLib;
using TSLib.Audio;
using TSLib.Full;
using TSLib.Helper;
namespace TS3AudioBot.Audio
{
internal class CustomTargetPipe : IVoiceTarget, IAudioPassiveConsumer
{
public TargetSendMode SendMode { get; set; } = TargetSendMode.Voice;
public ulong GroupWhisperTargetId { get; private set; }
public GroupWhisperType GroupWhisperType { get; private set; }
public GroupWhisperTarget GroupWhisperTarget { get; private set; }
public bool Alone { get; set; }
public IReadOnlyCollection WhisperClients
{
get { lock (subscriptionLockObj) { return clientSubscriptionsSetup.ToArray(); } }
}
public IReadOnlyCollection WhisperChannel
{
get { lock (subscriptionLockObj) { return channelSubscriptionsSetup.Keys.ToArray(); } }
}
public bool Active
{
get
{
switch (SendMode)
{
case TargetSendMode.None:
return false;
case TargetSendMode.Voice:
return !Alone;
case TargetSendMode.Whisper:
UpdatedSubscriptionCache();
return channelSubscriptionsCache.Length > 0 || clientSubscriptionsCache.Length > 0;
default:
return true;
}
}
}
private readonly Dictionary channelSubscriptionsSetup = new Dictionary();
private readonly HashSet clientSubscriptionsSetup = new HashSet();
private ChannelId[] channelSubscriptionsCache = Array.Empty();
private ClientId[] clientSubscriptionsCache = Array.Empty();
private bool subscriptionSetupChanged;
private readonly object subscriptionLockObj = new object();
private readonly TsFullClient client;
public CustomTargetPipe(TsFullClient client)
{
this.client = client;
subscriptionSetupChanged = true;
}
public void Write(Span data, Meta? meta)
{
UpdatedSubscriptionCache();
var codec = meta?.Codec ?? Codec.OpusMusic; // XXX a bit hacky
switch (SendMode)
{
case TargetSendMode.None:
break;
case TargetSendMode.Voice:
client.SendAudio(data, codec);
break;
case TargetSendMode.Whisper:
client.SendAudioWhisper(data, codec, channelSubscriptionsCache, clientSubscriptionsCache);
break;
case TargetSendMode.WhisperGroup:
client.SendAudioGroupWhisper(data, codec, GroupWhisperType, GroupWhisperTarget, GroupWhisperTargetId);
break;
default:
throw Tools.UnhandledDefault(SendMode);
}
}
#region ITargetManager
public void SetGroupWhisper(GroupWhisperType type, GroupWhisperTarget target, ulong targetId = 0)
{
GroupWhisperType = type;
GroupWhisperTarget = target;
GroupWhisperTargetId = targetId;
}
public void WhisperChannelSubscribe(bool temp, params ChannelId[] channels)
{
lock (subscriptionLockObj)
{
foreach (var channel in channels)
{
if (channelSubscriptionsSetup.TryGetValue(channel, out var subscriptionTemp))
{
channelSubscriptionsSetup[channel] = !subscriptionTemp || !temp;
}
else
{
channelSubscriptionsSetup[channel] = !temp;
subscriptionSetupChanged = true;
}
}
}
}
public void WhisperChannelUnsubscribe(bool temp, params ChannelId[] channels)
{
lock (subscriptionLockObj)
{
foreach (var channel in channels)
{
if (!temp)
{
subscriptionSetupChanged |= channelSubscriptionsSetup.Remove(channel);
}
else
{
if (channelSubscriptionsSetup.TryGetValue(channel, out bool subscriptionTemp) && subscriptionTemp)
{
channelSubscriptionsSetup.Remove(channel);
subscriptionSetupChanged = true;
}
}
}
}
}
public void WhisperClientSubscribe(params ClientId[] userId)
{
lock (subscriptionLockObj)
{
clientSubscriptionsSetup.UnionWith(userId);
subscriptionSetupChanged = true;
}
}
public void WhisperClientUnsubscribe(params ClientId[] userId)
{
lock (subscriptionLockObj)
{
clientSubscriptionsSetup.ExceptWith(userId);
subscriptionSetupChanged = true;
}
}
public void ClearTemporary()
{
lock (subscriptionLockObj)
{
var removeList = channelSubscriptionsSetup
.Where(kvp => kvp.Value)
.Select(kvp => kvp.Key)
.ToArray();
foreach (var chan in removeList)
{
channelSubscriptionsSetup.Remove(chan);
subscriptionSetupChanged = true;
}
}
}
private void UpdatedSubscriptionCache()
{
if (!subscriptionSetupChanged)
return;
lock (subscriptionLockObj)
{
if (!subscriptionSetupChanged)
return;
channelSubscriptionsCache = channelSubscriptionsSetup.Keys.ToArray();
clientSubscriptionsCache = clientSubscriptionsSetup.ToArray();
subscriptionSetupChanged = false;
}
}
#endregion
}
}
================================================
FILE: TS3AudioBot/Audio/FfmpegProducer.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using TS3AudioBot.Config;
using TS3AudioBot.Helper;
using TSLib.Audio;
using TSLib.Helper;
using TSLib.Scheduler;
namespace TS3AudioBot.Audio
{
public class FfmpegProducer : IPlayerSource, ISampleInfo, IDisposable
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
private readonly Id id;
private static readonly Regex FindDurationMatch = new Regex(@"^\s*Duration: (\d+):(\d\d):(\d\d).(\d\d)", Util.DefaultRegexConfig);
private static readonly Regex IcyMetadataMacher = new Regex("((\\w+)='(.*?)';\\s*)+", Util.DefaultRegexConfig);
private const string PreLinkConf = "-hide_banner -nostats -threads 1 -i \"";
private const string PostLinkConf = "\" -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1";
private const string LinkConfIcy = "-hide_banner -nostats -threads 1 -i pipe:0 -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1";
private static readonly TimeSpan retryOnDropBeforeEnd = TimeSpan.FromSeconds(10);
private readonly ConfToolsFfmpeg config;
public event EventHandler? OnSongEnd;
public event EventHandler? OnSongUpdated;
private readonly DedicatedTaskScheduler scheduler;
private FfmpegInstance? ffmpegInstance;
public int SampleRate { get; } = 48000;
public int Channels { get; } = 2;
public int BitsPerSample { get; } = 16;
public FfmpegProducer(ConfToolsFfmpeg config, DedicatedTaskScheduler scheduler, Id id)
{
this.config = config;
this.scheduler = scheduler;
this.id = id;
}
public Task AudioStart(string url, TimeSpan? startOff = null)
{
StartFfmpegProcess(url, startOff ?? TimeSpan.Zero);
return Task.CompletedTask;
}
public async Task AudioStartIcy(string url) => await StartFfmpegProcessIcy(url);
public void AudioStop()
{
StopFfmpegProcess();
}
public TimeSpan? Length => GetCurrentSongLength();
public TimeSpan? Position => ffmpegInstance?.AudioTimer.SongPosition;
public Task Seek(TimeSpan position) { SetPosition(position); return Task.CompletedTask; }
public int Read(byte[] buffer, int offset, int length, out Meta? meta)
{
meta = default;
int read;
var instance = ffmpegInstance;
if (instance is null)
return 0;
try
{
read = instance.FfmpegProcess.StandardOutput.BaseStream.Read(buffer, 0, length);
}
catch (Exception ex)
{
read = 0;
Log.Debug(ex, "Can't read ffmpeg");
}
if (read == 0)
{
AssertNotMainScheduler();
var (ret, triggerEndSafe) = instance.IsIcyStream
? OnReadEmptyIcy(instance)
: OnReadEmpty(instance);
if (ret)
return 0;
if (instance.FfmpegProcess.HasExitedSafe())
{
Log.Trace("Ffmpeg has exited");
AudioStop();
triggerEndSafe = true;
}
if (triggerEndSafe)
{
OnSongEnd?.Invoke(this, EventArgs.Empty);
return 0;
}
}
instance.HasTriedToReconnect = false;
instance.AudioTimer.PushBytes(read);
return read;
}
private (bool ret, bool trigger) OnReadEmpty(FfmpegInstance instance)
{
if (instance.FfmpegProcess.HasExitedSafe() && !instance.HasTriedToReconnect)
{
var expectedStopLength = GetCurrentSongLength();
Log.Trace("Expected song length {0}", expectedStopLength);
if (expectedStopLength != TimeSpan.Zero)
{
var actualStopPosition = instance.AudioTimer.SongPosition;
Log.Trace("Actual song position {0}", actualStopPosition);
if (actualStopPosition + retryOnDropBeforeEnd < expectedStopLength)
{
Log.Debug("Connection to song lost, retrying at {0}", actualStopPosition);
instance.HasTriedToReconnect = true;
var newInstance = SetPosition(actualStopPosition);
if (newInstance.Ok)
{
newInstance.Value.HasTriedToReconnect = true;
return (true, false);
}
else
{
Log.Debug("Retry failed {0}", newInstance.Error);
return (false, true);
}
}
}
}
return (false, false);
}
private (bool ret, bool trigger) OnReadEmptyIcy(FfmpegInstance instance)
{
AssertNotMainScheduler();
if (instance.FfmpegProcess.HasExitedSafe() && !instance.HasTriedToReconnect)
{
Log.Debug("Connection to stream lost, retrying...");
instance.HasTriedToReconnect = true;
var newInstance = StartFfmpegProcessIcy(instance.ReconnectUrl).Result;
if (newInstance.Ok)
{
newInstance.Value.HasTriedToReconnect = true;
return (true, false);
}
else
{
Log.Debug("Retry failed {0}", newInstance.Error);
return (false, true);
}
}
return (false, false);
}
private R SetPosition(TimeSpan value)
{
if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
var instance = ffmpegInstance;
if (instance is null)
return "No instance running";
if (instance.IsIcyStream)
return "Cannot seek icy stream";
var lastLink = instance.ReconnectUrl;
if (lastLink is null)
return "No current url active";
return StartFfmpegProcess(lastLink, value);
}
private R StartFfmpegProcess(string url, TimeSpan? offsetOpt)
{
StopFfmpegProcess();
Log.Trace("Start request {0}", url);
string arguments;
var offset = offsetOpt ?? TimeSpan.Zero;
if (offset > TimeSpan.Zero)
{
var seek = string.Format(CultureInfo.InvariantCulture, @"-ss {0:hh\:mm\:ss\.fff}", offset);
arguments = string.Concat(seek, " ", PreLinkConf, url, PostLinkConf, " ", seek);
}
else
{
arguments = string.Concat(PreLinkConf, url, PostLinkConf);
}
var newInstance = new FfmpegInstance(
url,
new PreciseAudioTimer(this)
{
SongPositionOffset = offset,
});
return StartFfmpegProcessInternal(newInstance, arguments);
}
private async Task> StartFfmpegProcessIcy(string url)
{
StopFfmpegProcess();
Log.Trace("Start icy-stream request {0}", url);
try
{
var response = await WebWrapper
.Request(url)
.WithHeader("Icy-MetaData", "1")
.UnsafeResponse();
if (!int.TryParse(response.Headers.GetSingle("icy-metaint"), out var metaint))
{
response.Dispose();
return "Invalid icy stream tags";
}
var stream = await response.Content.ReadAsStreamAsync();
var newInstance = new FfmpegInstance(
url,
new PreciseAudioTimer(this),
stream,
metaint)
{
OnMetaUpdated = e => OnSongUpdated?.Invoke(this, e)
};
new Thread(() => newInstance.ReadStreamLoop(id))
{
Name = $"IcyStreamReader[{id}]",
}.Start();
return StartFfmpegProcessInternal(newInstance, LinkConfIcy);
}
catch (Exception ex)
{
var error = $"Unable to create icy-stream ({ex.Message})";
Log.Warn(ex, error);
return error;
}
}
private R StartFfmpegProcessInternal(FfmpegInstance instance, string arguments)
{
try
{
instance.FfmpegProcess.StartInfo = new ProcessStartInfo
{
FileName = config.Path.Value,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardInput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
instance.FfmpegProcess.EnableRaisingEvents = true;
Log.Debug("Starting ffmpeg with {0}", arguments);
instance.FfmpegProcess.ErrorDataReceived += instance.FfmpegProcess_ErrorDataReceived;
instance.FfmpegProcess.Start();
instance.FfmpegProcess.BeginErrorReadLine();
instance.AudioTimer.Start();
var oldInstance = Interlocked.Exchange(ref ffmpegInstance, instance);
oldInstance?.Close();
return instance;
}
catch (Exception ex)
{
var error = ex is Win32Exception
? $"Ffmpeg could not be found ({ex.Message})"
: $"Unable to create stream ({ex.Message})";
Log.Error(ex, error);
instance.Close();
StopFfmpegProcess();
return error;
}
}
private void StopFfmpegProcess()
{
var oldInstance = Interlocked.Exchange(ref ffmpegInstance, null);
if (oldInstance != null)
{
oldInstance.OnMetaUpdated = null;
oldInstance.Close();
}
}
private TimeSpan? GetCurrentSongLength() => ffmpegInstance?.ParsedSongLength;
private void AssertNotMainScheduler()
{
if (TaskScheduler.Current == scheduler)
throw new Exception("Cannot read on own scheduler. Throwing to prevent deadlock");
}
public void Dispose()
{
StopFfmpegProcess();
}
private class FfmpegInstance
{
public Process FfmpegProcess { get; }
public bool HasTriedToReconnect { get; set; }
public string ReconnectUrl { get; }
public bool IsIcyStream => IcyStream != null;
public PreciseAudioTimer AudioTimer { get; }
public TimeSpan? ParsedSongLength { get; set; } = null;
public Stream? IcyStream { get; }
public int IcyMetaInt { get; }
public bool Closed { get; set; }
public Action? OnMetaUpdated;
public FfmpegInstance(string url, PreciseAudioTimer timer) : this(url, timer, null!, 0) { }
public FfmpegInstance(string url, PreciseAudioTimer timer, Stream icyStream, int icyMetaInt)
{
FfmpegProcess = new Process();
ReconnectUrl = url;
AudioTimer = timer;
IcyStream = icyStream;
IcyMetaInt = icyMetaInt;
HasTriedToReconnect = false;
}
public void Close()
{
Closed = true;
try
{
if (!FfmpegProcess.HasExitedSafe())
FfmpegProcess.Kill();
}
catch { }
try { FfmpegProcess.CancelErrorRead(); } catch { }
try { FfmpegProcess.StandardInput.Dispose(); } catch { }
try { FfmpegProcess.StandardOutput.Dispose(); } catch { }
try { FfmpegProcess.Dispose(); } catch { }
IcyStream?.Dispose();
}
public void FfmpegProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data is null)
return;
if (sender != FfmpegProcess)
throw new InvalidOperationException("Wrong process associated to event");
if (ParsedSongLength is null)
{
var match = FindDurationMatch.Match(e.Data);
if (!match.Success)
return;
int hours = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
int minutes = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture);
int seconds = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture);
int millisec = int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture) * 10;
ParsedSongLength = new TimeSpan(0, hours, minutes, seconds, millisec);
}
//if (!HasIcyTag && e.Data.AsSpan().TrimStart().StartsWith("icy-".AsSpan()))
//{
// HasIcyTag = true;
//}
}
public void ReadStreamLoop(Id id)
{
if (IcyStream is null)
throw new InvalidOperationException("Instance is not an icy stream");
Tools.SetLogId(id.ToString());
const int IcyMaxMeta = 255 * 16;
const int ReadBufferSize = 4096;
int errorCount = 0;
var buffer = new byte[Math.Max(ReadBufferSize, IcyMaxMeta)];
int readCount = 0;
while (!Closed)
{
try
{
while (readCount < IcyMetaInt)
{
int read = IcyStream.Read(buffer, 0, Math.Min(ReadBufferSize, IcyMetaInt - readCount));
if (read == 0)
{
Close();
return;
}
readCount += read;
FfmpegProcess.StandardInput.BaseStream.Write(buffer, 0, read);
errorCount = 0;
}
readCount = 0;
var metaByte = IcyStream.ReadByte();
if (metaByte < 0)
{
Close();
return;
}
if (metaByte > 0)
{
metaByte *= 16;
while (readCount < metaByte)
{
int read = IcyStream.Read(buffer, 0, metaByte - readCount);
if (read == 0)
{
Close();
return;
}
readCount += read;
}
readCount = 0;
var metaString = Tools.Utf8Encoder.GetString(buffer, 0, metaByte).TrimEnd('\0');
Log.Debug("Meta: {0}", metaString);
OnMetaUpdated?.Invoke(ParseIcyMeta(metaString));
}
}
catch (Exception ex)
{
errorCount++;
if (errorCount >= 50)
{
Log.Error(ex, "Failed too many times trying to access ffmpeg. Closing stream.");
Close();
return;
}
if (ex is InvalidOperationException)
{
Log.Debug(ex, "Waiting for ffmpeg");
Thread.Sleep(100);
}
else
{
Log.Debug(ex, "Stream read/write error");
}
}
}
}
private static SongInfoChanged ParseIcyMeta(string metaString)
{
var songInfo = new SongInfoChanged();
var match = IcyMetadataMacher.Match(metaString);
if (match.Success)
{
for (int i = 0; i < match.Groups[1].Captures.Count; i++)
{
switch (match.Groups[2].Captures[i].Value.ToUpperInvariant())
{
case "STREAMTITLE":
songInfo.Title = match.Groups[3].Captures[i].Value;
break;
}
}
}
return songInfo;
}
}
}
}
================================================
FILE: TS3AudioBot/Audio/IPlayerSource.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Threading.Tasks;
using TSLib.Audio;
namespace TS3AudioBot.Audio
{
public interface IPlayerSource : IAudioPassiveProducer
{
event EventHandler OnSongEnd;
event EventHandler OnSongUpdated;
TimeSpan? Length { get; }
TimeSpan? Position { get; }
Task Seek(TimeSpan position);
}
}
================================================
FILE: TS3AudioBot/Audio/IVoiceTarget.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Collections.Generic;
using TSLib;
using TSLib.Audio;
namespace TS3AudioBot.Audio
{
/// Used to specify playing mode and active targets to send to.
public interface IVoiceTarget
{
TargetSendMode SendMode { get; set; }
ulong GroupWhisperTargetId { get; }
GroupWhisperType GroupWhisperType { get; }
GroupWhisperTarget GroupWhisperTarget { get; }
void SetGroupWhisper(GroupWhisperType type, GroupWhisperTarget target, ulong targetId);
IReadOnlyCollection WhisperClients { get; }
IReadOnlyCollection WhisperChannel { get; }
/// Adds a channel to the audio streaming list.
/// When set to true this channel will be cleared with
/// the next call (unless overwritten with false).
/// The id of the channel.
void WhisperChannelSubscribe(bool temp, params ChannelId[] channel);
/// Removes a channel from the audio streaming list.
/// When set to true this channel will be cleared with
/// the next call (unless overwritten with false).
/// The id of the channel.
void WhisperChannelUnsubscribe(bool temp, params ChannelId[] channel);
void ClearTemporary();
void WhisperClientSubscribe(params ClientId[] userId);
void WhisperClientUnsubscribe(params ClientId[] userId);
}
}
================================================
FILE: TS3AudioBot/Audio/PlayInfo.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Diagnostics.CodeAnalysis;
using TSLib;
namespace TS3AudioBot.Audio
{
public sealed class PlayInfo
{
/// Defaults to: invoker.Uid - Can be set if the owner of a song differs from the invoker.
public Uid? ResourceOwnerUid { get; set; }
/// Starts the song at the specified time if set.
public TimeSpan? StartOffset { get; set; }
public PlayInfo(TimeSpan? startOffset = null)
{
StartOffset = startOffset;
}
public PlayInfo Merge(PlayInfo other) => Merge(this, other);
[return: NotNullIfNotNull("self")]
[return: NotNullIfNotNull("other")]
public static PlayInfo? Merge(PlayInfo? self, PlayInfo? other)
{
if (other is null)
return self;
if (self is null)
return other;
self.ResourceOwnerUid ??= other.ResourceOwnerUid;
self.StartOffset ??= other.StartOffset;
return self;
}
public static PlayInfo MergeDefault(PlayInfo? self, PlayInfo? other)
=> Merge(self, other) ?? new PlayInfo();
}
public interface IMetaContainer
{
public PlayInfo? PlayInfo { get; set; }
}
public static class MetaContainerExtensions
{
public static T MergeMeta(this T container, PlayInfo? other) where T : IMetaContainer
{
container.PlayInfo = PlayInfo.Merge(container.PlayInfo, other);
return container;
}
}
}
================================================
FILE: TS3AudioBot/Audio/PlayInfoEventArgs.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using TS3AudioBot.ResourceFactories;
namespace TS3AudioBot.Audio
{
public sealed class PlayInfoEventArgs : EventArgs
{
public InvokerData Invoker { get; }
public PlayResource PlayResource { get; }
public AudioResource ResourceData => PlayResource.AudioResource;
public PlayInfo? PlayInfo => PlayResource.PlayInfo;
public string? SourceLink { get; }
public PlayInfoEventArgs(InvokerData invoker, PlayResource playResource, string? sourceLink)
{
Invoker = invoker;
PlayResource = playResource;
SourceLink = sourceLink;
}
}
}
================================================
FILE: TS3AudioBot/Audio/PlayManager.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TS3AudioBot.Config;
using TS3AudioBot.Environment;
using TS3AudioBot.Helper;
using TS3AudioBot.Localization;
using TS3AudioBot.Playlists;
using TS3AudioBot.ResourceFactories;
using TSLib.Helper;
namespace TS3AudioBot.Audio
{
/// Provides a convenient inferface for enqueing, playing and registering song events.
public class PlayManager
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
private readonly ConfBot confBot;
private readonly Player playerConnection;
private readonly PlaylistManager playlistManager;
private readonly ResolveContext resourceResolver;
private readonly Stats stats;
public PlayInfoEventArgs? CurrentPlayData { get; private set; }
public bool IsPlaying => CurrentPlayData != null;
public event AsyncEventHandler? OnResourceUpdated;
public event AsyncEventHandler? BeforeResourceStarted;
public event AsyncEventHandler? AfterResourceStarted;
public event AsyncEventHandler? ResourceStopped;
public event AsyncEventHandler? PlaybackStopped;
public PlayManager(ConfBot config, Player playerConnection, PlaylistManager playlistManager, ResolveContext resourceResolver, Stats stats)
{
confBot = config;
this.playerConnection = playerConnection;
this.playlistManager = playlistManager;
this.resourceResolver = resourceResolver;
this.stats = stats;
}
public Task Enqueue(InvokerData invoker, AudioResource ar, PlayInfo? meta = null) => Enqueue(invoker, new PlaylistItem(ar, meta));
public async Task Enqueue(InvokerData invoker, string message, string? audioType = null, PlayInfo? meta = null)
{
PlayResource playResource;
try { playResource = await resourceResolver.Load(message, audioType); }
catch
{
stats.TrackSongLoad(audioType, false, true);
throw;
}
await Enqueue(invoker, PlaylistItem.From(playResource).MergeMeta(meta));
}
public Task Enqueue(InvokerData invoker, IEnumerable items)
{
var startOff = playlistManager.CurrentList.Items.Count;
playlistManager.Queue(items.Select(x => UpdateItem(invoker, x)));
return PostEnqueue(invoker, startOff);
}
public Task Enqueue(InvokerData invoker, PlaylistItem item)
{
var startOff = playlistManager.CurrentList.Items.Count;
playlistManager.Queue(UpdateItem(invoker, item));
return PostEnqueue(invoker, startOff);
}
private static PlaylistItem UpdateItem(InvokerData invoker, PlaylistItem item)
{
item.PlayInfo ??= new PlayInfo();
item.PlayInfo.ResourceOwnerUid = invoker.ClientUid;
return item;
}
private async Task PostEnqueue(InvokerData invoker, int startIndex)
{
if (IsPlaying)
return;
playlistManager.Index = startIndex;
await StartCurrent(invoker);
}
/// Tries to play the passed
/// The invoker of this resource. Used for responses and association.
/// The resource to load and play.
/// Allows overriding certain settings for the resource. Can be null.
/// Ok if successful, or an error message otherwise.
public async Task Play(InvokerData invoker, AudioResource ar, PlayInfo? meta = null)
{
if (ar is null)
throw new ArgumentNullException(nameof(ar));
PlayResource playResource;
try { playResource = await resourceResolver.Load(ar); }
catch
{
stats.TrackSongLoad(ar.AudioType, false, true);
throw;
}
await Play(invoker, playResource.MergeMeta(meta));
}
/// Tries to play the passed link.
/// The invoker of this resource. Used for responses and association.
/// The link to resolve, load and play.
/// The associated resource type string to a factory.
/// Allows overriding certain settings for the resource. Can be null.
/// Ok if successful, or an error message otherwise.
public async Task Play(InvokerData invoker, string link, string? audioType = null, PlayInfo? meta = null)
{
PlayResource playResource;
try { playResource = await resourceResolver.Load(link, audioType); }
catch
{
stats.TrackSongLoad(audioType, false, true);
throw;
}
await Play(invoker, playResource.MergeMeta(meta));
}
public Task Play(InvokerData invoker, IEnumerable items, int index = 0)
{
playlistManager.Clear();
playlistManager.Queue(items.Select(x => UpdateItem(invoker, x)));
playlistManager.Index = index;
return StartCurrent(invoker);
}
public Task Play(InvokerData invoker, PlaylistItem item)
{
if (item is null)
throw new ArgumentNullException(nameof(item));
if (item.AudioResource is null)
throw new Exception("Invalid playlist item");
playlistManager.Clear();
playlistManager.Queue(item);
playlistManager.Index = 0;
return StartResource(invoker, item);
}
public Task Play(InvokerData invoker) => StartCurrent(invoker);
/// Plays the passed
/// The invoker of this resource. Used for responses and association.
/// The associated resource type string to a factory.
/// Allows overriding certain settings for the resource.
/// Ok if successful, or an error message otherwise.
public Task Play(InvokerData invoker, PlayResource play)
{
playlistManager.Clear();
playlistManager.Queue(PlaylistItem.From(play));
playlistManager.Index = 0;
stats.TrackSongLoad(play.AudioResource.AudioType, true, true);
return StartResource(invoker, play);
}
private async Task StartResource(InvokerData invoker, PlaylistItem item)
{
PlayResource playResource;
try { playResource = await resourceResolver.Load(item.AudioResource); }
catch
{
stats.TrackSongLoad(item.AudioResource.AudioType, false, false);
throw;
}
stats.TrackSongLoad(item.AudioResource.AudioType, true, false);
await StartResource(invoker, playResource.MergeMeta(item.PlayInfo));
}
private async Task StartResource(InvokerData invoker, PlayResource play)
{
var sourceLink = resourceResolver.RestoreLink(play.AudioResource);
var playInfo = new PlayInfoEventArgs(invoker, play, sourceLink);
await BeforeResourceStarted.InvokeAsync(this, playInfo);
if (string.IsNullOrWhiteSpace(play.PlayUri))
{
Log.Error("Internal resource error: link is empty (resource:{0})", play);
throw Error.LocalStr(strings.error_playmgr_internal_error);
}
Log.Debug("AudioResource start: {0}", play);
try { await playerConnection.Play(play); }
catch (AudioBotException ex)
{
Log.Error("Error return from player: {0}", ex.Message);
throw Error.Exception(ex).LocalStr(strings.error_playmgr_internal_error);
}
playerConnection.Volume = Tools.Clamp(playerConnection.Volume, confBot.Audio.Volume.Min, confBot.Audio.Volume.Max);
CurrentPlayData = playInfo; // TODO meta as readonly
await AfterResourceStarted.InvokeAsync(this, playInfo);
}
private async Task StartCurrent(InvokerData invoker, bool manually = true)
{
var pli = playlistManager.Current;
if (pli is null)
throw Error.LocalStr(strings.error_playlist_is_empty);
try
{
await StartResource(invoker, pli);
}
catch (AudioBotException ex)
{
Log.Warn("Skipping: {0} because {1}", pli, ex.Message);
await Next(invoker, manually);
}
}
public async Task Next(InvokerData invoker, bool manually = true)
{
PlaylistItem? pli = null;
for (int i = 0; i < 10; i++)
{
pli = playlistManager.Next(manually);
if (pli is null) break;
try
{
await StartResource(invoker, pli);
return;
}
catch (AudioBotException ex) { Log.Warn("Skipping: {0} because {1}", pli, ex.Message); }
}
if (pli is null)
throw Error.LocalStr(strings.info_playmgr_no_next_song);
else
throw Error.LocalStr(string.Format(strings.error_playmgr_many_songs_failed, "!next"));
}
public async Task Previous(InvokerData invoker, bool manually = true)
{
PlaylistItem? pli = null;
for (int i = 0; i < 10; i++)
{
pli = playlistManager.Previous(manually);
if (pli is null) break;
try
{
await StartResource(invoker, pli);
return;
}
catch (AudioBotException ex) { Log.Warn("Skipping: {0} because {1}", pli, ex.Message); }
}
if (pli is null)
throw Error.LocalStr(strings.info_playmgr_no_previous_song);
else
throw Error.LocalStr(string.Format(strings.error_playmgr_many_songs_failed, "!previous"));
}
public async Task SongStoppedEvent(object? sender, EventArgs e) => await StopInternal(true);
public Task Stop() => StopInternal(false);
private async Task StopInternal(bool songEndedByCallback)
{
await ResourceStopped.InvokeAsync(this, new SongEndEventArgs(songEndedByCallback));
if (songEndedByCallback)
{
try
{
await Next(CurrentPlayData?.Invoker ?? InvokerData.Anonymous, false);
return;
}
catch (AudioBotException ex) { Log.Info("Song queue ended: {0}", ex.Message); }
}
else
{
playerConnection.Stop();
}
CurrentPlayData = null;
PlaybackStopped?.Invoke(this, EventArgs.Empty);
}
public async Task Update(SongInfoChanged newInfo)
{
var data = CurrentPlayData;
if (data is null)
return;
if (newInfo.Title != null)
data.ResourceData.ResourceTitle = newInfo.Title;
// further properties...
try
{
await OnResourceUpdated.InvokeAsync(this, data);
}
catch (AudioBotException ex)
{
Log.Warn(ex, "Error in OnResourceUpdated event.");
}
}
public static PlayInfo? ParseAttributes(string[] attrs)
{
if (attrs is null || attrs.Length == 0)
return null;
var meta = new PlayInfo();
foreach (var attr in attrs)
{
if (attr.StartsWith("@"))
{
meta.StartOffset = TextUtil.ParseTime(attr.Substring(1));
}
}
return meta;
}
}
}
================================================
FILE: TS3AudioBot/Audio/Player.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Threading.Tasks;
using TS3AudioBot.Config;
using TS3AudioBot.Helper;
using TS3AudioBot.ResourceFactories;
using TSLib;
using TSLib.Audio;
using TSLib.Helper;
using TSLib.Scheduler;
namespace TS3AudioBot.Audio
{
public class Player : IDisposable
{
private const Codec SendCodec = Codec.OpusMusic;
private readonly DedicatedTaskScheduler scheduler;
public IPlayerSource? CurrentPlayerSource { get; private set; }
public StallCheckPipe StallCheckPipe { get; }
public VolumePipe VolumePipe { get; }
public FfmpegProducer FfmpegProducer { get; }
public PreciseTimedPipe TimePipe { get; }
public PassiveMergePipe MergePipe { get; }
public EncoderPipe EncoderPipe { get; }
public IAudioPassiveConsumer? PlayerSink { get; private set; }
public Player(ConfRoot confRoot, ConfBot config, DedicatedTaskScheduler scheduler, Id id)
{
this.scheduler = scheduler;
FfmpegProducer = new FfmpegProducer(confRoot.Tools.Ffmpeg, scheduler, id);
StallCheckPipe = new StallCheckPipe();
VolumePipe = new VolumePipe();
Volume = config.Audio.Volume.Default;
EncoderPipe = new EncoderPipe(SendCodec) { Bitrate = ScaleBitrate(config.Audio.Bitrate) };
TimePipe = new PreciseTimedPipe(EncoderPipe, id) { ReadBufferSize = EncoderPipe.PacketSize };
MergePipe = new PassiveMergePipe();
config.Audio.Bitrate.Changed += (s, e) => EncoderPipe.Bitrate = ScaleBitrate(e.NewValue);
MergePipe.Into(TimePipe).Chain().Chain(StallCheckPipe).Chain(VolumePipe).Chain(EncoderPipe);
}
public void SetTarget(IAudioPassiveConsumer target)
{
PlayerSink = target;
EncoderPipe.Chain(target);
}
private static int ScaleBitrate(int value) => Tools.Clamp(value, 1, 255) * 1000;
public event AsyncEventHandler? OnSongEnd;
public event AsyncEventHandler? OnSongUpdated;
private void TriggerSongEnd(object? o, EventArgs e) => scheduler.InvokeAsync(() => OnSongEnd.InvokeAsync(this, EventArgs.Empty));
private void TriggerSongUpdated(object? o, SongInfoChanged e) => scheduler.InvokeAsync(() => OnSongUpdated.InvokeAsync(this, e));
public async Task Play(PlayResource res)
{
if (res is MediaPlayResource mres && mres.IsIcyStream)
await FfmpegProducer.AudioStartIcy(res.PlayUri);
else
await FfmpegProducer.AudioStart(res.PlayUri, res.PlayInfo?.StartOffset);
Play(FfmpegProducer);
}
public void Play(IPlayerSource source)
{
var oldSource = CurrentPlayerSource;
if (oldSource != source)
{
// Clean up old
CleanSource(oldSource);
// Set events
source.OnSongEnd += TriggerSongEnd;
source.OnSongUpdated += TriggerSongUpdated;
// Update pipes
MergePipe.Add(source);
CurrentPlayerSource = source;
}
// Start Ticker
TimePipe.AudioTimer.Reset();
TimePipe.Paused = false;
}
private void CleanSource(IPlayerSource? source)
{
if (source is null)
return;
source.OnSongEnd -= TriggerSongEnd;
source.OnSongUpdated -= TriggerSongUpdated;
MergePipe.Remove(source);
source.Dispose();
}
public void Stop()
{
CurrentPlayerSource?.Dispose();
if (MergePipe.Count <= 1)
TimePipe.Paused = true;
}
public void StopAll()
{
Stop();
TimePipe.Paused = true;
MergePipe.Dispose();
}
public TimeSpan? Length => CurrentPlayerSource?.Length;
public TimeSpan? Position => CurrentPlayerSource?.Position;
public Task Seek(TimeSpan position) => CurrentPlayerSource?.Seek(position) ?? Task.CompletedTask;
public float Volume
{
get => AudioValues.FactorToHumanVolume(VolumePipe.Volume);
set => VolumePipe.Volume = AudioValues.HumanVolumeToFactor(value);
}
public bool Paused
{
get => TimePipe.Paused;
set => TimePipe.Paused = value;
}
// Extras
public void SetStall() => StallCheckPipe.SetStall();
[Obsolete(AttributeStrings.UnderDevelopment)]
public void MixInStreamOnce(IPlayerSource producer)
{
producer.OnSongEnd += (s, e) =>
{
MergePipe.Remove(producer);
producer.Dispose();
};
MergePipe.Add(producer);
TimePipe.Paused = false;
}
public void Dispose()
{
StopAll();
CleanSource(CurrentPlayerSource);
TimePipe.Dispose();
FfmpegProducer.Dispose();
EncoderPipe.Dispose();
}
}
}
================================================
FILE: TS3AudioBot/Audio/SongEndEventArgs.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
namespace TS3AudioBot.Audio
{
public class SongEndEventArgs : EventArgs
{
public bool SongEndedByCallback { get; }
public SongEndEventArgs(bool songEndedByCallback) { SongEndedByCallback = songEndedByCallback; }
}
}
================================================
FILE: TS3AudioBot/Audio/SongInfoChanged.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
namespace TS3AudioBot.Audio
{
public class SongInfoChanged : EventArgs
{
public string? Title { get; set; }
}
}
================================================
FILE: TS3AudioBot/Audio/StallCheckPipe.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using TSLib.Audio;
namespace TS3AudioBot.Audio
{
public class StallCheckPipe : IAudioPipe
{
private const uint StallCountInterval = 10;
private const uint StallNoErrorCountMax = 5;
public bool Active => OutStream?.Active ?? false;
public IAudioPassiveConsumer? OutStream { get; set; }
private bool isStall;
private uint stallCount;
private uint stallNoErrorCount;
public StallCheckPipe()
{
isStall = false;
stallCount = 0;
}
public void Write(Span data, Meta? meta)
{
if (OutStream is null) return;
if (isStall)
{
// TODO maybe do time-cooldown instead of call-count-cooldown
if (++stallCount % StallCountInterval == 0)
{
stallNoErrorCount++;
if (stallNoErrorCount > StallNoErrorCountMax)
{
stallCount = 0;
isStall = false;
}
}
else
{
return;
}
}
OutStream?.Write(data, meta);
}
public void SetStall()
{
stallNoErrorCount = 0;
isStall = true;
}
}
}
================================================
FILE: TS3AudioBot/Audio/StreamAudioPlayerSource.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Threading.Tasks;
using TSLib.Audio;
namespace TS3AudioBot.Audio
{
public class StreamAudioPlayerSource : IPlayerSource, IAudioActiveConsumer
{
private bool hasFired = false;
public IAudioPassiveProducer? InStream { get; set; }
public TimeSpan? Length => null;
public TimeSpan? Position => null;
public event EventHandler? OnSongEnd;
event EventHandler IPlayerSource.OnSongUpdated { add { } remove { } }
public StreamAudioPlayerSource() { }
public StreamAudioPlayerSource(IAudioPassiveProducer stream) : this()
{
InStream = stream;
}
public int Read(byte[] buffer, int offset, int length, out Meta? meta)
{
var stream = InStream;
if (stream is null)
{
meta = default;
return 0;
}
var read = stream.Read(buffer, offset, length, out meta);
if (read == 0 && !hasFired)
{
hasFired = true;
OnSongEnd?.Invoke(this, EventArgs.Empty);
return 0;
}
return read;
}
public void Reset() => hasFired = false;
public void Dispose() => InStream?.Dispose();
public Task Seek(TimeSpan position) { throw new NotSupportedException(); }
}
}
================================================
FILE: TS3AudioBot/Bot.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using TS3AudioBot.Algorithm;
using TS3AudioBot.Audio;
using TS3AudioBot.CommandSystem;
using TS3AudioBot.CommandSystem.Text;
using TS3AudioBot.Config;
using TS3AudioBot.Dependency;
using TS3AudioBot.Environment;
using TS3AudioBot.Helper;
using TS3AudioBot.History;
using TS3AudioBot.Localization;
using TS3AudioBot.Playlists;
using TS3AudioBot.Plugins;
using TS3AudioBot.ResourceFactories;
using TS3AudioBot.Sessions;
using TSLib;
using TSLib.Full;
using TSLib.Helper;
using TSLib.Messages;
using TSLib.Scheduler;
namespace TS3AudioBot
{
/// Core class managing all bots and utility modules.
public sealed class Bot
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
private readonly ConfBot config;
private readonly TickWorker idleTickWorker;
private bool isClosed;
internal BotInjector Injector { get; }
internal DedicatedTaskScheduler Scheduler { get; }
public Id Id { get; }
/// This is the template name. Can be null.
public string? Name => config.Name;
public bool QuizMode { get; set; }
private readonly ResolveContext resourceResolver;
private readonly Ts3Client ts3client;
private readonly TsFullClient ts3FullClient;
private readonly SessionManager sessionManager;
private readonly PlayManager playManager;
private readonly IVoiceTarget targetManager;
private readonly Player player;
private readonly Stats stats;
private readonly LocalizationManager localization;
public Bot(Id id, ConfBot config, BotInjector injector)
{
this.Id = id;
this.config = config;
this.Injector = injector;
// Registering config changes
config.Language.Changed += async (s, e) =>
{
var langResult = await localization.LoadLanguage(e.NewValue, true);
if (!langResult.Ok)
Log.Error("Failed to load language file ({0})", langResult.Error);
};
config.Events.IdleDelay.Changed += (s, e) => EnableIdleTickWorker();
config.Events.OnIdle.Changed += (s, e) => EnableIdleTickWorker();
var builder = new DependencyBuilder(Injector);
Injector.HideParentModule();
Injector.HideParentModule();
Injector.AddModule(this);
Injector.AddModule(config);
Injector.AddModule(Injector);
Injector.AddModule(config.Playlists);
Injector.AddModule(config.History);
Injector.AddModule(Id);
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
builder.RequestModule();
if (config.History.Enabled)
{
builder.RequestModule();
}
builder.RequestModule();
if (!builder.Build())
{
Log.Error("Missing bot module dependency");
throw new Exception("Could not load all bot modules");
}
Injector.ClearHiddenParentModules();
resourceResolver = Injector.GetModuleOrThrow();
ts3FullClient = Injector.GetModuleOrThrow();
ts3client = Injector.GetModuleOrThrow();
player = Injector.GetModuleOrThrow();
Scheduler = Injector.GetModuleOrThrow();
var customTarget = Injector.GetModuleOrThrow();
player.SetTarget(customTarget);
Injector.AddModule(ts3FullClient.Book);
playManager = Injector.GetModuleOrThrow();
targetManager = Injector.GetModuleOrThrow();
sessionManager = Injector.GetModuleOrThrow();
stats = Injector.GetModuleOrThrow();
var commandManager = Injector.GetModuleOrThrow();
localization = Injector.GetModuleOrThrow();
idleTickWorker = Scheduler.Invoke(() => Scheduler.CreateTimer(OnIdle, TimeSpan.MaxValue, false)).Result;
player.OnSongEnd += playManager.SongStoppedEvent;
player.OnSongUpdated += (s, e) => playManager.Update(e);
// Update idle status events
playManager.BeforeResourceStarted += (s, e) => { DisableIdleTickWorker(); return Task.CompletedTask; };
playManager.PlaybackStopped += (s, e) => { EnableIdleTickWorker(); return Task.CompletedTask; };
// Used for custom scripts, like voice_mode, onsongstart
playManager.BeforeResourceStarted += BeforeResourceStarted;
playManager.AfterResourceStarted += AfterResourceStarted;
// Update the own status text to the current song title
playManager.AfterResourceStarted += (s, e) => UpdateBotStatus();
playManager.PlaybackStopped += (s, e) => UpdateBotStatus();
playManager.OnResourceUpdated += (s, e) => UpdateBotStatus();
// Log our resource in the history
if (Injector.TryGet(out var historyManager))
playManager.AfterResourceStarted += (s, e) =>
{
if (e.PlayInfo != null)
historyManager.LogAudioResource(new HistorySaveData(e.PlayResource.AudioResource, e.PlayInfo.ResourceOwnerUid));
return Task.CompletedTask;
};
// Update our thumbnail
playManager.AfterResourceStarted += (s, e) => GenerateStatusImage(true, e);
playManager.PlaybackStopped += (s, e) => GenerateStatusImage(false, null);
// Stats
playManager.AfterResourceStarted += (s, e) => { stats.TrackSongStart(Id, e.ResourceData.AudioType); return Task.CompletedTask; };
playManager.ResourceStopped += (s, e) => { stats.TrackSongStop(Id); return Task.CompletedTask; };
// Register callback for all messages happening
ts3client.OnMessageReceived += OnMessageReceived;
// Register callback to remove open private sessions, when user disconnects
ts3FullClient.OnEachClientLeftView += OnClientLeftView;
ts3client.OnBotConnected += OnBotConnected;
ts3client.OnBotDisconnected += OnBotDisconnected;
ts3client.OnBotStoppedReconnecting += OnBotStoppedReconnecting;
// Alone mode
ts3client.OnAloneChanged += OnAloneChanged;
ts3client.OnAloneChanged += (s, e) => { customTarget.Alone = e.Alone; return Task.CompletedTask; };
// Whisper stall
ts3client.OnWhisperNoTarget += (s, e) => player.SetStall();
commandManager.RegisterCollection(MainCommands.Bag);
// TODO remove after plugin rework
var pluginManager = Injector.GetModuleOrThrow();
foreach (var plugin in pluginManager.Plugins)
if (plugin.Type == PluginType.CorePlugin || plugin.Type == PluginType.Commands)
commandManager.RegisterCollection(plugin.CorePlugin.Bag);
// Restore all alias from the config
foreach (var alias in config.Commands.Alias.GetAllItems())
commandManager.RegisterAlias(alias.Key, alias.Value).UnwrapToLog(Log);
}
public Task> Run()
{
Scheduler.VerifyOwnThread();
Log.Info("Bot \"{0}\" connecting to \"{1}\"", config.Name, config.Connect.Address);
return Task.FromResult(ts3client.Connect());
}
public async Task Stop()
{
Scheduler.VerifyOwnThread();
Injector.GetModule()?.RemoveBot(this);
if (!isClosed) isClosed = true;
else return;
Log.Info("Bot ({0}) disconnecting.", Id);
DisableIdleTickWorker();
Injector.GetModule()?.StopPlugins(this);
Injector.GetModule()?.Stop();
Injector.GetModule()?.Dispose();
var tsClient = Injector.GetModule();
if (tsClient != null)
await tsClient.Disconnect();
Injector.GetModule()?.Dispose();
config.ClearEvents();
}
private async Task OnBotConnected(object? sender, EventArgs e)
{
EnableIdleTickWorker();
var badges = config.Connect.Badges.Value;
if (!string.IsNullOrEmpty(badges))
ts3client?.ChangeBadges(badges);
var onStart = config.Events.OnConnect.Value;
if (!string.IsNullOrEmpty(onStart))
{
var info = CreateExecInfo();
await CallScript(info, onStart, false, true);
}
}
private async Task OnBotDisconnected(object? sender, DisconnectEventArgs e)
{
DisableIdleTickWorker();
var onStop = config.Events.OnDisconnect.Value;
if (!string.IsNullOrEmpty(onStop))
{
var info = CreateExecInfo();
await CallScript(info, onStop, false, true);
}
}
private Task OnBotStoppedReconnecting(object? sender, EventArgs e)
{
return Stop();
}
private async Task OnMessageReceived(object? sender, TextMessage textMessage)
{
if (textMessage?.Message == null)
{
Log.Warn("Invalid TextMessage: {@textMessage}", textMessage);
return;
}
Log.Debug("TextMessage: {@textMessage}", textMessage);
if (!localization.LanguageLoaded)
{
var langResult = await localization.LoadLanguage(config.Language, false);
if (!langResult.Ok)
Log.Error("Failed to load language file ({0})", langResult.Error);
}
localization.ApplyLanguage();
textMessage.Message = textMessage.Message.TrimStart(' ');
if (!textMessage.Message.StartsWith("!", StringComparison.Ordinal))
return;
Log.Info("User {0} requested: {1}", textMessage.InvokerName, textMessage.Message);
ts3client.InvalidateClientBuffer();
ChannelId? channelId = null;
ClientDbId? databaseId = null;
ChannelGroupId? channelGroup = null;
ServerGroupId[]? serverGroups = null;
if (ts3FullClient.Book.Clients.TryGetValue(textMessage.InvokerId, out var bookClient))
{
channelId = bookClient.Channel;
databaseId = bookClient.DatabaseId;
serverGroups = bookClient.ServerGroups.ToArray();
channelGroup = bookClient.ChannelGroup;
}
else if ((await ts3FullClient.ClientInfo(textMessage.InvokerId)).Get(out var infoClient, out var infoClientError))
{
channelId = infoClient.ChannelId;
databaseId = infoClient.DatabaseId;
serverGroups = infoClient.ServerGroups;
channelGroup = infoClient.ChannelGroup;
}
else
{
try
{
var cachedClient = await ts3client.GetCachedClientById(textMessage.InvokerId);
channelId = cachedClient.ChannelId;
databaseId = cachedClient.DatabaseId;
channelGroup = cachedClient.ChannelGroup;
}
catch (AudioBotException cachedClientError)
{
Log.Warn(
"The bot is missing teamspeak permissions to view the communicating client. " +
"Some commands or permission checks might not work " +
"(clientlist:{0}, clientinfo:{1}).",
cachedClientError.Message, infoClientError.ErrorFormat());
}
}
var invoker = new ClientCall(textMessage.InvokerUid ?? Uid.Anonymous, textMessage.Message,
clientId: textMessage.InvokerId,
visibiliy: textMessage.Target,
nickName: textMessage.InvokerName,
channelId: channelId,
databaseId: databaseId,
serverGroups: serverGroups,
channelGroup: channelGroup);
var session = sessionManager.GetOrCreateSession(textMessage.InvokerId);
var info = CreateExecInfo(invoker, session);
// check if the user has an open request
if (session.ResponseProcessor != null)
{
await TryCatchCommand(info, answer: true, async () =>
{
var msg = await session.ResponseProcessor(textMessage.Message);
if (!string.IsNullOrEmpty(msg))
await info.Write(msg).CatchToLog(Log);
});
session.ClearResponse();
return;
}
await CallScript(info, textMessage.Message, answer: true, false);
}
private void OnClientLeftView(object? sender, ClientLeftView eventArgs)
{
targetManager.WhisperClientUnsubscribe(eventArgs.ClientId);
sessionManager.RemoveSession(eventArgs.ClientId);
}
private async Task BeforeResourceStarted(object? sender, PlayInfoEventArgs e)
{
const string DefaultVoiceScript = "!whisper off";
const string DefaultWhisperScript = "!xecute (!whisper subscription) (!unsubscribe temporary) (!subscribe channeltemp (!getmy channel))";
var mode = config.Audio.SendMode.Value;
string script;
if (mode.StartsWith("!", StringComparison.Ordinal))
script = mode;
else if (mode.Equals("voice", StringComparison.OrdinalIgnoreCase))
script = DefaultVoiceScript;
else if (mode.Equals("whisper", StringComparison.OrdinalIgnoreCase))
script = DefaultWhisperScript;
else
{
Log.Error("Invalid voice mode");
return;
}
var info = CreateExecInfo(e.Invoker);
await CallScript(info, script, false, true);
}
private async Task AfterResourceStarted(object? sender, PlayInfoEventArgs e)
{
var onSongStart = config.Events.OnSongStart.Value;
if (!string.IsNullOrEmpty(onSongStart))
{
var info = CreateExecInfo();
await CallScript(info, onSongStart, false, true);
}
}
#region Status: Description, Avatar
public Task UpdateBotStatus()
{
return Scheduler.InvokeAsync(UpdateBotStatusInternal);
}
private async Task UpdateBotStatusInternal()
{
Scheduler.VerifyOwnThread();
if (isClosed)
return;
if (!config.SetStatusDescription)
return;
string? setString;
if (playManager.IsPlaying)
{
setString = QuizMode
? strings.info_botstatus_quiztime
: playManager.CurrentPlayData?.ResourceData?.ResourceTitle;
}
else
{
setString = strings.info_botstatus_sleeping;
}
}
public Task GenerateStatusImage(bool playing, PlayInfoEventArgs? startEvent)
{
return Scheduler.InvokeAsync(() => GenerateStatusImageInternal(playing, startEvent));
}
private async Task GenerateStatusImageInternal(bool playing, PlayInfoEventArgs? startEvent)
{
Scheduler.VerifyOwnThread();
if (!config.GenerateStatusAvatar || isClosed)
return;
static Stream? GetRandomFile(string? basePath, string prefix)
{
try
{
if (string.IsNullOrEmpty(basePath))
return null;
var avatarPath = new DirectoryInfo(Path.Combine(basePath, BotPaths.Avatars));
if (!avatarPath.Exists)
return null;
var avatars = avatarPath.EnumerateFiles(prefix).ToArray();
if (avatars.Length == 0)
return null;
var pickedAvatar = Tools.PickRandom(avatars);
return pickedAvatar.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
}
catch (Exception ex)
{
Log.Warn(ex, "Failed to load local avatar");
return null;
}
}
Stream? setStream = null;
if (playing)
{
if (startEvent != null && !QuizMode)
{
try
{
await resourceResolver.GetThumbnail(startEvent.PlayResource,
async thumbStream => setStream = (await ImageUtil.ResizeImageSave(thumbStream)).Stream);
}
catch (AudioBotException ex) { Log.Debug(ex, "Failed to fetch thumbnail image"); }
}
setStream ??= GetRandomFile(config.LocalConfigDir, "play*");
}
else
{
setStream ??= GetRandomFile(config.LocalConfigDir, "sleep*");
setStream ??= Util.GetEmbeddedFile("TS3AudioBot.Media.SleepingKitty.png");
var result = await ts3FullClient.UploadAvatar(setStream);
result.UnwrapToLog(Log);
}
if (setStream != null)
{
}
else
{
var result = await ts3FullClient.DeleteAvatar();
result.UnwrapToLog(Log);
}
}
#endregion
#region Script Execution
private async Task CallScript(ExecutionInformation info, string command, bool answer, bool skipRights)
{
Log.Debug("Calling script (skipRights:{0}, answer:{1}): {2}", skipRights, answer, command);
stats.TrackCommandCall(answer);
info.AddModule(new CallerInfo(false)
{
SkipRightsChecks = skipRights,
CommandComplexityMax = config.Commands.CommandComplexity,
IsColor = config.Commands.Color,
});
await TryCatchCommand(info, answer, async () =>
{
// parse and execute the command
var res = await CommandManager.Execute(info, command);
if (!answer)
return;
// Write result to user
var s = res.AsString();
if (!string.IsNullOrEmpty(s))
await info.Write(s).CatchToLog(Log);
});
}
private ExecutionInformation CreateExecInfo(InvokerData? invoker = null, UserSession? session = null)
{
var info = new ExecutionInformation(Injector);
if (invoker is ClientCall ci)
info.AddModule(ci);
info.AddModule(invoker ?? InvokerData.Anonymous);
info.AddModule(session ?? new AnonymousSession());
info.AddModule(Filter.GetFilterByNameOrDefault(config.Commands.Matcher));
return info;
}
private async Task TryCatchCommand(ExecutionInformation info, bool answer, Func action)
{
try
{
await action.Invoke();
}
catch (AudioBotException ex)
{
NLog.LogLevel commandErrorLevel = answer ? NLog.LogLevel.Debug : NLog.LogLevel.Warn;
Log.Log(commandErrorLevel, ex, "Command Error ({0})", ex.Message);
if (answer)
{
await info.Write(TextMod.Format(config.Commands.Color, strings.error_call_error.Mod().Color(Color.Red).Bold(), ex.Message))
.CatchToLog(Log);
}
}
catch (Exception ex)
{
Log.Error(ex, "Unexpected command error: {0}", ex.Message);
if (answer)
{
await info.Write(TextMod.Format(config.Commands.Color, strings.error_call_unexpected_error.Mod().Color(Color.Red).Bold(), ex.Message))
.CatchToLog(Log);
}
}
}
#endregion
#region Event: Idle
private async void OnIdle()
{
// DisableIdleTickWorker(); // fire once only ??
var onIdle = config.Events.OnIdle.Value;
if (!string.IsNullOrEmpty(onIdle))
{
var info = CreateExecInfo();
await CallScript(info, onIdle, false, true);
}
}
private void EnableIdleTickWorker()
{
var idleTime = config.Events.IdleDelay.Value;
if (idleTime <= TimeSpan.Zero || string.IsNullOrEmpty(config.Events.OnIdle.Value))
{
DisableIdleTickWorker();
return;
}
idleTickWorker.Interval = idleTime;
idleTickWorker.Enable();
}
private void DisableIdleTickWorker() => idleTickWorker.Disable();
#endregion
#region Event: Alone/Party
private async Task OnAloneChanged(object? sender, AloneChanged e)
{
Scheduler.VerifyOwnThread();
string script;
TimeSpan delay;
if (e.Alone)
{
script = config.Events.OnAlone.Value;
delay = config.Events.AloneDelay.Value;
}
else
{
script = config.Events.OnParty.Value;
delay = config.Events.PartyDelay.Value;
}
if (string.IsNullOrEmpty(script))
return;
if (delay > TimeSpan.Zero) // TODO: Async (Add cancellation token for better consistency)
await Task.Delay(delay);
var info = CreateExecInfo();
await CallScript(info, script, false, true);
}
#endregion
public BotInfo GetInfo() => new BotInfo
{
Id = Id,
Name = config.Name,
Server = ts3FullClient.ConnectionData?.Address,
Status = ts3FullClient.Connected ? BotStatus.Connected : BotStatus.Connecting,
};
}
public class BotInfo
{
public int? Id { get; set; }
public string? Name { get; set; }
public string? Server { get; set; }
public BotStatus Status { get; set; }
public override string ToString() => $"Id: {Id} Name: {Name} Server: {Server} Status: {Status}"; // LOC: TODO
}
public enum BotStatus
{
Offline,
Connecting,
Connected,
}
}
================================================
FILE: TS3AudioBot/BotManager.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TS3AudioBot.Config;
using TS3AudioBot.Dependency;
using TS3AudioBot.Helper;
using TSLib;
using TSLib.Helper;
namespace TS3AudioBot
{
public class BotManager
{
private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger();
private List? activeBots = new List();
private readonly object lockObj = new object();
private readonly ConfRoot confRoot;
private readonly CoreInjector coreInjector;
public BotManager(ConfRoot confRoot, CoreInjector coreInjector)
{
this.confRoot = confRoot;
this.coreInjector = coreInjector;
}
public async Task RunBots(bool interactive)
{
var botConfigList = confRoot.GetAllBots();
if (botConfigList is null)
return;
if (botConfigList.Length == 0)
{
if (!interactive)
{
Log.Warn("No bots are configured in the load list.");
return;
}
Console.WriteLine("It seems like there are no bots configured.");
Console.WriteLine("Fill out this quick setup to get started.");
var newBot = CreateNewBot();
newBot.Run.Value = true;
var address = await Interactive.LoopActionAsync("Please enter the ip, domain or nickname (with port; default: 9987) where to connect to:", async addr =>
{
if (await TsDnsResolver.TryResolve(addr) != null)
return true;
Console.WriteLine("The address seems invalid or could not be resolved, continue anyway? [y/N]");
return Interactive.UserAgree(defaultTo: false);
});
if (address is null)
return;
newBot.Connect.Address.Value = address;
Console.WriteLine("Please enter the server password (or leave empty for none):");
newBot.Connect.ServerPassword.Password.Value = Console.ReadLine();
if (!newBot.SaveNew(ConfigHelper.DefaultBotName))
{
Log.Error("Could not save new bot. Ensure that the bot has access to the directory.");
return;
}
if (!confRoot.Save())
Log.Error("Could not save root config. The bot won't start by default.");
var runResult = await RunBot(newBot);
if (!runResult.Ok)
Log.Error("Could not run bot ({0})", runResult.Error);
return;
}
var launchBotTasks = new List>>(botConfigList.Length);
foreach (var instance in botConfigList)
{
if (!instance.Run)
continue;
launchBotTasks.Add(RunBot(instance).ContinueWith(async t =>
{
var result = await t;
if (!result.Ok)
{
Log.Error("Could not instantiate bot: {0}", result.Error);
}
return result;
}).Unwrap());
}
await Task.WhenAll(launchBotTasks);
}
public ConfBot CreateNewBot() => confRoot.CreateBot();
public Task> CreateAndRunNewBot()
{
var botConf = CreateNewBot();
return RunBot(botConf);
}
public async Task> RunBotTemplate(string name)
{
var config = confRoot.GetBotConfig(name);
if (!config.Ok)
return config.Error.Message;
return await RunBot(config.Value);
}
public async Task> RunBot(ConfBot config)
{
var (bot, info) = InstantiateNewBot(config);
if (info != null)
return info;
if (bot is null)
return "Failed to create new Bot";
return await bot.Scheduler.InvokeAsync>(async () =>
{
var initializeResult = await bot.Run();
if (!initializeResult.Ok)
{
await StopBot(bot);
return $"Bot failed to initialize ({initializeResult.Error})";
}
return bot.GetInfo();
});
}
private (Bot? bot, BotInfo? info) InstantiateNewBot(ConfBot config)
{
lock (lockObj)
{
if (!string.IsNullOrEmpty(config.Name))
{
var maybeBot = GetBotSave(config.Name);
if (maybeBot != null)
return (null, maybeBot.GetInfo());
}
var id = GetFreeId();
if (id == null)
return (null, null); // "BotManager is shutting down"
var botInjector = new BotInjector(coreInjector);
botInjector.AddModule(botInjector);
botInjector.AddModule(new Id(id.Value));
botInjector.AddModule(config);
if (!botInjector.TryCreate(out var bot))
return (null, null); // "Failed to create new Bot"
InsertBot(bot);
return (bot, null);
}
}
// !! This method must be called with a lock on lockObj
private void InsertBot(Bot bot)
{
if (activeBots is null)
return;
activeBots[bot.Id] = bot;
}
// !! This method must be called with a lock on lockObj
// !! The id must be either used withing the same lock or considered invalid.
private int? GetFreeId()
{
if (activeBots is null)
return null;
for (int i = 0; i < activeBots.Count; i++)
{
if (activeBots[i] is null)
{
return i;
}
}
// All slots are full, get a new slot
activeBots.Add(null);
return activeBots.Count - 1;
}
// !! This method must be called with a lock on lockObj
private Bot? GetBotSave(int id)
{
if (activeBots is null || id < 0 || id >= activeBots.Count)
return null;
return activeBots[id];
}
// !! This method must be called with a lock on lockObj
private Bot? GetBotSave(string name)
{
if (name is null)
throw new ArgumentNullException(nameof(name));
if (activeBots is null)
return null;
return activeBots.Find(x => x?.Name == name);
}
public Bot? GetBotLock(int id)
{
Bot? bot;
lock (lockObj)
{
bot = GetBotSave(id);
if (bot is null)
return null;
if (bot.Id != id)
throw new Exception("Got not matching bot id");
}
return bot;
}
public Bot? GetBotLock(string name)
{
Bot? bot;
lock (lockObj)
{
bot = GetBotSave(name);
if (bot is null)
return null;
if (bot.Name != name)
throw new Exception("Got not matching bot name");
}
return bot;
}
internal void IterateAll(Action body)
{
lock (lockObj)
{
if (activeBots is null)
return;
foreach (var bot in activeBots)
{
if (bot is null) continue;
body(bot);
}
}
}
public async Task StopBot(Bot bot)
{
RemoveBot(bot);
await bot.Scheduler.InvokeAsync(async () => await bot.Stop());
}
internal void RemoveBot(Bot bot)
{
lock (lockObj)
{
Bot? botInList;
if (activeBots != null
&& (botInList = GetBotSave(bot.Id)) != null
&& botInList == bot)
{
activeBots[bot.Id] = null;
}
}
}
public BotInfo[] GetBotInfolist()
{
lock (lockObj)
{
if (activeBots is null)
return Array.Empty();
return activeBots.Where(x => x != null).Select(x => x!.GetInfo()).ToArray();
}
}
public uint GetRunningBotCount()
{
lock (lockObj)
{
if (activeBots is null)
return 0;
return (uint)activeBots.Count(x => x != null);
}
}
public async Task StopBots()
{
List disposeBots;
lock (lockObj)
{
if (activeBots is null)
return;
disposeBots = activeBots;
activeBots = null;
}
await Task.WhenAll(disposeBots.Where(x => x != null).Select(x => StopBot(x!)));
}
}
}
================================================
FILE: TS3AudioBot/CallerInfo.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
namespace TS3AudioBot
{
public class CallerInfo
{
/// Whether this call was initiated from the api.
public bool ApiCall { get; }
/// Skips all permission checks when set to true.
public bool SkipRightsChecks { get; set; } = false;
/// Counts execution token for a single call to prevent endless loops.
public int CommandComplexityCurrent { get; set; } = 0;
/// The maximum execution token count for a single call.
public int CommandComplexityMax { get; set; } = 0;
/// Whether the caller wants a colored output.
public bool IsColor { get; set; }
public CallerInfo(bool isApi)
{
ApiCall = isApi;
}
}
}
================================================
FILE: TS3AudioBot/ClientCall.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using TSLib;
namespace TS3AudioBot
{
public class ClientCall : InvokerData
{
/// The original unmodified string which was received by the client.
public string TextMessage { get; }
public ClientDbId? DatabaseId { get; }
public ChannelId? ChannelId { get; }
public ClientId? ClientId { get; }
public string? NickName { get; }
public ServerGroupId[]? ServerGroups { get; }
public ChannelGroupId? ChannelGroup { get; }
public TextMessageTargetMode? Visibiliy { get; internal set; }
public ClientCall(Uid clientUid, string textMessage, ClientDbId? databaseId = null,
ChannelId? channelId = null, ClientId? clientId = null, string? nickName = null,
TextMessageTargetMode? visibiliy = null, ServerGroupId[]? serverGroups = null,
ChannelGroupId? channelGroup = null) : base(clientUid)
{
TextMessage = textMessage;
DatabaseId = databaseId;
ChannelId = channelId;
ClientId = clientId;
NickName = nickName;
Visibiliy = visibiliy;
ServerGroups = serverGroups;
ChannelGroup = channelGroup;
}
}
}
================================================
FILE: TS3AudioBot/CommandSystem/Ast/AstCommand.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Collections.Generic;
using System.Text;
namespace TS3AudioBot.CommandSystem.Ast
{
internal class AstCommand : AstNode
{
public override AstType Type => AstType.Command;
public List Parameter { get; } = new List();
public AstCommand(string fullRequest) : base(fullRequest) { }
public override void Write(StringBuilder strb, int depth)
{
strb.Space(depth);
if (Parameter.Count == 0)
{
strb.Append("");
}
else
{
if (Parameter[0] is AstValue comName)
strb.Append("!").Append(comName.Value);
else
strb.Append("");
for (int i = 1; i < Parameter.Count; i++)
{
strb.AppendLine();
Parameter[i].Write(strb, depth + 1);
}
}
}
}
}
================================================
FILE: TS3AudioBot/CommandSystem/Ast/AstError.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Text;
namespace TS3AudioBot.CommandSystem.Ast
{
internal class AstError : AstNode
{
public override AstType Type => AstType.Error;
public string Description { get; }
public AstError(AstNode referenceNode, string description)
: this(referenceNode.FullRequest, referenceNode.Position, referenceNode.Length, description) { }
public AstError(string request, int pos, int len, string description) : base(request)
{
Position = pos;
Length = len;
Description = description;
}
public override void Write(StringBuilder strb, int depth)
{
strb.AppendLine(FullRequest);
if (Position == 1) strb.Append('.');
else if (Position > 1) strb.Append(' ', Position);
strb.Append('~', Length).Append('^').AppendLine();
strb.Append("Error: ").AppendLine(Description);
}
}
}
================================================
FILE: TS3AudioBot/CommandSystem/Ast/AstNode.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Text;
namespace TS3AudioBot.CommandSystem.Ast
{
public abstract class AstNode
{
public abstract AstType Type { get; }
public string FullRequest { get; }
public int Position { get; set; }
public int Length { get; set; }
protected AstNode(string fullRequest)
{
FullRequest = fullRequest;
}
public abstract void Write(StringBuilder strb, int depth);
public sealed override string ToString()
{
var strb = new StringBuilder();
Write(strb, 0);
return strb.ToString();
}
}
internal static class AstNodeExtensions
{
public const int SpacePerTab = 2;
public static StringBuilder Space(this StringBuilder strb, int depth) => strb.Append(' ', depth * SpacePerTab);
}
}
================================================
FILE: TS3AudioBot/CommandSystem/Ast/AstType.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
namespace TS3AudioBot.CommandSystem.Ast
{
public enum AstType
{
Command,
Value,
Error,
}
}
================================================
FILE: TS3AudioBot/CommandSystem/Ast/AstValue.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using System.Text;
namespace TS3AudioBot.CommandSystem.Ast
{
internal class AstValue : AstNode
{
private string? value;
private string? tailString;
public override AstType Type => AstType.Value;
public StringType StringType { get; }
public int TailLength { get; set; }
public string Value
{
get => value ??= FullRequest.Substring(Position, Length);
set { this.value = value; tailString = value; }
}
public string TailString
{
get
{
if (tailString is null)
{
if (TailLength == 0)
tailString = FullRequest.Substring(Position);
else
tailString = FullRequest.Substring(Position, TailLength);
}
return tailString;
}
}
public AstValue(string fullRequest, StringType stringType) : base(fullRequest)
{
StringType = stringType;
}
public override void Write(StringBuilder strb, int depth) => strb.Space(depth).Append(Value);
}
}
================================================
FILE: TS3AudioBot/CommandSystem/Ast/StringType.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
namespace TS3AudioBot.CommandSystem.Ast
{
internal enum StringType
{
FreeString,
QuotedString,
}
}
================================================
FILE: TS3AudioBot/CommandSystem/BotCommand.cs
================================================
// TS3AudioBot - An advanced Musicbot for Teamspeak 3
// Copyright (C) 2017 TS3AudioBot contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the Open Software License v. 3.0
//
// You should have received a copy of the Open Software License along with this
// program. If not, see .
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using TS3AudioBot.CommandSystem.Commands;
using TS3AudioBot.Localization;
namespace TS3AudioBot.CommandSystem
{
[DebuggerDisplay("{DebuggerDisplay, nq}")]
[JsonObject(MemberSerialization.OptIn)]
public class BotCommand : FunctionCommand
{
private readonly string helpLookupName;
private string? cachedFullQualifiedName;
[JsonProperty(PropertyName = "Name")]
public string InvokeName { get; }
private readonly string[] requiredRights;
public string RequiredRight => requiredRights[0];
[JsonProperty(PropertyName = "Description")]
public string? Description => LocalizationManager.GetString(helpLookupName);
public UsageAttribute[] UsageList { get; }
public string FullQualifiedName
{
get
{
if (cachedFullQualifiedName is null)
{
var strb = new StringBuilder();
strb.Append(InvokeName);
strb.Append(" (");
strb.Append(string.Join(", ", CommandParameter.Where(p => !p.Kind.IsNormal()).Select(p => p.Type.FullName).OrderBy(p => p)));
strb.Append("|");
strb.Append(string.Join(", ", CommandParameter.Where(p => p.Kind.IsNormal()).Select(p => p.Type.FullName)));
strb.Append(")");
cachedFullQualifiedName = strb.ToString();
}
return cachedFullQualifiedName;
}
}
[JsonProperty(PropertyName = "Return")]
public string Return { get; set; }
[JsonProperty(PropertyName = "Parameter")]
public (string name, string type, bool optional)[] Parameter { get; }
[JsonProperty(PropertyName = "Modules")]
public (string type, bool optional)[] Modules { get; }
public string DebuggerDisplay
{
get
{
var strb = new StringBuilder();
strb.Append('!').Append(InvokeName);
strb.Append(" : ");
foreach (var param in UsageList)
strb.Append(param.UsageSyntax).Append('/');
return strb.ToString();
}
}
public BotCommand(CommandBuildInfo buildInfo) : base(buildInfo.Method, buildInfo.Parent)
{
InvokeName = buildInfo.CommandData.CommandNameSpace;
helpLookupName = buildInfo.CommandData.OverrideHelpName ?? ("cmd_" + InvokeName.Replace(" ", "_") + "_help");
requiredRights = new[] { "cmd." + string.Join(".", InvokeName.Split(' ')) };
UsageList = buildInfo.UsageList;
// Serialization
Return = UnwrapReturnType(CommandReturn).Name;
Parameter = (
from x in CommandParameter
where x.Kind.IsNormal()
select (x.Name, UnwrapParamType(x.Type).Name, x.Optional)).ToArray();
Modules = (
from x in CommandParameter
where x.Kind == ParamKind.Dependency
select (x.Type.Name, x.Optional)).ToArray();
}
public override string ToString()
{
var strb = new StringBuilder();
strb.Append("\n!")
.Append(InvokeName);
foreach (var (name, _, optional) in Parameter)
{
strb.Append(' ');
if (optional)
strb.Append("[<").Append(name).Append(">]");
else
strb.Append('<').Append(name).Append('>');
}
strb.Append(": ")
.Append(Description ?? strings.error_no_help ?? "");
if (UsageList.Length > 0)
{
int longest = UsageList.Max(p => p.UsageSyntax.Length) + 1;
foreach (var para in UsageList)
strb.Append("\n!").Append(InvokeName).Append(" ").Append(para.UsageSyntax)
.Append(' ', longest - para.UsageSyntax.Length).Append(para.UsageHelp);
}
return strb.ToString();
}
public override async ValueTask