要提交的变更: 修改: README.md 新文件: README_zh-CN.md 新文件: assets/CHANGELOG.md 新文件: assets/Parsedown.php 新文件: assets/css/login.css 新文件: assets/css/manage.css 新文件: assets/defaultConfig.json 新文件: assets/defaultIconList.json 新文件: assets/html/favicon.ico 新文件: assets/html/login.html 新文件: assets/html/manage.html 新文件: assets/js/console.js 新文件: assets/js/manage.js 新文件: assets/opencc/composer.json 新文件: assets/opencc/composer.lock 新文件: assets/opencc/vendor/autoload.php 新文件: assets/opencc/vendor/bin/opencc 新文件: assets/opencc/vendor/composer/ClassLoader.php 新文件: assets/opencc/vendor/composer/InstalledVersions.php 新文件: assets/opencc/vendor/composer/LICENSE 新文件: assets/opencc/vendor/composer/autoload_classmap.php 新文件: assets/opencc/vendor/composer/autoload_files.php 新文件: assets/opencc/vendor/composer/autoload_namespaces.php 新文件: assets/opencc/vendor/composer/autoload_psr4.php 新文件: assets/opencc/vendor/composer/autoload_real.php 新文件: assets/opencc/vendor/composer/autoload_static.php 新文件: assets/opencc/vendor/composer/installed.json 新文件: assets/opencc/vendor/composer/installed.php 新文件: assets/opencc/vendor/composer/platform_check.php 新文件: assets/opencc/vendor/overtrue/php-opencc/.editorconfig 新文件: assets/opencc/vendor/overtrue/php-opencc/.github/FUNDING.yml 新文件: assets/opencc/vendor/overtrue/php-opencc/.github/workflows/test.yml 新文件: assets/opencc/vendor/overtrue/php-opencc/LICENSE 新文件: assets/opencc/vendor/overtrue/php-opencc/README.md 新文件: assets/opencc/vendor/overtrue/php-opencc/bin/build 新文件: assets/opencc/vendor/overtrue/php-opencc/bin/opencc 新文件: assets/opencc/vendor/overtrue/php-opencc/composer.json 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/HKVariants.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/HKVariantsRevPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/JPShinjitaiCharacters.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/JPShinjitaiPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/JPVariants.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/README.md 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/STCharacters.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/STPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TSCharacters.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TSPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWPhrasesIT.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWPhrasesName.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWPhrasesOther.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWVariants.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/dictionary/TWVariantsRevPhrases.txt 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/HKVariants.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/HKVariantsRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/HKVariantsRevPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPShinjitaiCharacters.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPShinjitaiPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPVariants.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/JPVariantsRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/STCharacters.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/STPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TSCharacters.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TSPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesIT.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesName.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesOther.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWPhrasesRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWVariants.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWVariantsRev.php 新文件: assets/opencc/vendor/overtrue/php-opencc/data/parsed/TWVariantsRevPhrases.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Console/BuildCommand.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Console/ConvertCommand.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Contracts/ConverterInterface.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Converter.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Dictionary.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/OpenCC.php 新文件: assets/opencc/vendor/overtrue/php-opencc/src/Strategy.php 新文件: assets/opencc/vendor/psr/container/.gitignore 新文件: assets/opencc/vendor/psr/container/LICENSE 新文件: assets/opencc/vendor/psr/container/README.md 新文件: assets/opencc/vendor/psr/container/composer.json 新文件: assets/opencc/vendor/psr/container/src/ContainerExceptionInterface.php 新文件: assets/opencc/vendor/psr/container/src/ContainerInterface.php 新文件: assets/opencc/vendor/psr/container/src/NotFoundExceptionInterface.php 新文件: assets/opencc/vendor/symfony/console/Application.php 新文件: assets/opencc/vendor/symfony/console/Attribute/AsCommand.php 新文件: assets/opencc/vendor/symfony/console/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/console/CI/GithubActionReporter.php 新文件: assets/opencc/vendor/symfony/console/Color.php 新文件: assets/opencc/vendor/symfony/console/Command/Command.php 新文件: assets/opencc/vendor/symfony/console/Command/CompleteCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/DumpCompletionCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/HelpCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/LazyCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/ListCommand.php 新文件: assets/opencc/vendor/symfony/console/Command/LockableTrait.php 新文件: assets/opencc/vendor/symfony/console/Command/SignalableCommandInterface.php 新文件: assets/opencc/vendor/symfony/console/Command/TraceableCommand.php 新文件: assets/opencc/vendor/symfony/console/CommandLoader/CommandLoaderInterface.php 新文件: assets/opencc/vendor/symfony/console/CommandLoader/ContainerCommandLoader.php 新文件: assets/opencc/vendor/symfony/console/CommandLoader/FactoryCommandLoader.php 新文件: assets/opencc/vendor/symfony/console/Completion/CompletionInput.php 新文件: assets/opencc/vendor/symfony/console/Completion/CompletionSuggestions.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/BashCompletionOutput.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/CompletionOutputInterface.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/FishCompletionOutput.php 新文件: assets/opencc/vendor/symfony/console/Completion/Output/ZshCompletionOutput.php 新文件: assets/opencc/vendor/symfony/console/Completion/Suggestion.php 新文件: assets/opencc/vendor/symfony/console/ConsoleEvents.php 新文件: assets/opencc/vendor/symfony/console/Cursor.php 新文件: assets/opencc/vendor/symfony/console/DataCollector/CommandDataCollector.php 新文件: assets/opencc/vendor/symfony/console/Debug/CliRequest.php 新文件: assets/opencc/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/ApplicationDescription.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/Descriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/DescriptorInterface.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/JsonDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/MarkdownDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/ReStructuredTextDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/TextDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Descriptor/XmlDescriptor.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleCommandEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleErrorEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleSignalEvent.php 新文件: assets/opencc/vendor/symfony/console/Event/ConsoleTerminateEvent.php 新文件: assets/opencc/vendor/symfony/console/EventListener/ErrorListener.php 新文件: assets/opencc/vendor/symfony/console/Exception/CommandNotFoundException.php 新文件: assets/opencc/vendor/symfony/console/Exception/ExceptionInterface.php 新文件: assets/opencc/vendor/symfony/console/Exception/InvalidArgumentException.php 新文件: assets/opencc/vendor/symfony/console/Exception/InvalidOptionException.php 新文件: assets/opencc/vendor/symfony/console/Exception/LogicException.php 新文件: assets/opencc/vendor/symfony/console/Exception/MissingInputException.php 新文件: assets/opencc/vendor/symfony/console/Exception/NamespaceNotFoundException.php 新文件: assets/opencc/vendor/symfony/console/Exception/RunCommandFailedException.php 新文件: assets/opencc/vendor/symfony/console/Exception/RuntimeException.php 新文件: assets/opencc/vendor/symfony/console/Formatter/NullOutputFormatter.php 新文件: assets/opencc/vendor/symfony/console/Formatter/NullOutputFormatterStyle.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatter.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterInterface.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterStyle.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterStyleInterface.php 新文件: assets/opencc/vendor/symfony/console/Formatter/OutputFormatterStyleStack.php 新文件: assets/opencc/vendor/symfony/console/Formatter/WrappableOutputFormatterInterface.php 新文件: assets/opencc/vendor/symfony/console/Helper/DebugFormatterHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/DescriptorHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/Dumper.php 新文件: assets/opencc/vendor/symfony/console/Helper/FormatterHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/Helper.php 新文件: assets/opencc/vendor/symfony/console/Helper/HelperInterface.php 新文件: assets/opencc/vendor/symfony/console/Helper/HelperSet.php 新文件: assets/opencc/vendor/symfony/console/Helper/InputAwareHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/OutputWrapper.php 新文件: assets/opencc/vendor/symfony/console/Helper/ProcessHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/ProgressBar.php 新文件: assets/opencc/vendor/symfony/console/Helper/ProgressIndicator.php 新文件: assets/opencc/vendor/symfony/console/Helper/QuestionHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/SymfonyQuestionHelper.php 新文件: assets/opencc/vendor/symfony/console/Helper/Table.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableCell.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableCellStyle.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableRows.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableSeparator.php 新文件: assets/opencc/vendor/symfony/console/Helper/TableStyle.php 新文件: assets/opencc/vendor/symfony/console/Input/ArgvInput.php 新文件: assets/opencc/vendor/symfony/console/Input/ArrayInput.php 新文件: assets/opencc/vendor/symfony/console/Input/Input.php 新文件: assets/opencc/vendor/symfony/console/Input/InputArgument.php 新文件: assets/opencc/vendor/symfony/console/Input/InputAwareInterface.php 新文件: assets/opencc/vendor/symfony/console/Input/InputDefinition.php 新文件: assets/opencc/vendor/symfony/console/Input/InputInterface.php 新文件: assets/opencc/vendor/symfony/console/Input/InputOption.php 新文件: assets/opencc/vendor/symfony/console/Input/StreamableInputInterface.php 新文件: assets/opencc/vendor/symfony/console/Input/StringInput.php 新文件: assets/opencc/vendor/symfony/console/LICENSE 新文件: assets/opencc/vendor/symfony/console/Logger/ConsoleLogger.php 新文件: assets/opencc/vendor/symfony/console/Messenger/RunCommandContext.php 新文件: assets/opencc/vendor/symfony/console/Messenger/RunCommandMessage.php 新文件: assets/opencc/vendor/symfony/console/Messenger/RunCommandMessageHandler.php 新文件: assets/opencc/vendor/symfony/console/Output/AnsiColorMode.php 新文件: assets/opencc/vendor/symfony/console/Output/BufferedOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/ConsoleOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/ConsoleOutputInterface.php 新文件: assets/opencc/vendor/symfony/console/Output/ConsoleSectionOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/NullOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/Output.php 新文件: assets/opencc/vendor/symfony/console/Output/OutputInterface.php 新文件: assets/opencc/vendor/symfony/console/Output/StreamOutput.php 新文件: assets/opencc/vendor/symfony/console/Output/TrimmedBufferOutput.php 新文件: assets/opencc/vendor/symfony/console/Question/ChoiceQuestion.php 新文件: assets/opencc/vendor/symfony/console/Question/ConfirmationQuestion.php 新文件: assets/opencc/vendor/symfony/console/Question/Question.php 新文件: assets/opencc/vendor/symfony/console/README.md 新文件: assets/opencc/vendor/symfony/console/Resources/bin/hiddeninput.exe 新文件: assets/opencc/vendor/symfony/console/Resources/completion.bash 新文件: assets/opencc/vendor/symfony/console/Resources/completion.fish 新文件: assets/opencc/vendor/symfony/console/Resources/completion.zsh 新文件: assets/opencc/vendor/symfony/console/SignalRegistry/SignalMap.php 新文件: assets/opencc/vendor/symfony/console/SignalRegistry/SignalRegistry.php 新文件: assets/opencc/vendor/symfony/console/SingleCommandApplication.php 新文件: assets/opencc/vendor/symfony/console/Style/OutputStyle.php 新文件: assets/opencc/vendor/symfony/console/Style/StyleInterface.php 新文件: assets/opencc/vendor/symfony/console/Style/SymfonyStyle.php 新文件: assets/opencc/vendor/symfony/console/Terminal.php 新文件: assets/opencc/vendor/symfony/console/Tester/ApplicationTester.php 新文件: assets/opencc/vendor/symfony/console/Tester/CommandCompletionTester.php 新文件: assets/opencc/vendor/symfony/console/Tester/CommandTester.php 新文件: assets/opencc/vendor/symfony/console/Tester/Constraint/CommandIsSuccessful.php 新文件: assets/opencc/vendor/symfony/console/Tester/TesterTrait.php 新文件: assets/opencc/vendor/symfony/console/composer.json 新文件: assets/opencc/vendor/symfony/deprecation-contracts/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/deprecation-contracts/LICENSE 新文件: assets/opencc/vendor/symfony/deprecation-contracts/README.md 新文件: assets/opencc/vendor/symfony/deprecation-contracts/composer.json 新文件: assets/opencc/vendor/symfony/deprecation-contracts/function.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/Ctype.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-ctype/README.md 新文件: assets/opencc/vendor/symfony/polyfill-ctype/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-ctype/composer.json 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/Grapheme.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/README.md 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-grapheme/composer.json 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Normalizer.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/README.md 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-intl-normalizer/composer.json 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/LICENSE 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Mbstring.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/README.md 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/bootstrap.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/bootstrap80.php 新文件: assets/opencc/vendor/symfony/polyfill-mbstring/composer.json 新文件: assets/opencc/vendor/symfony/process/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/process/Exception/ExceptionInterface.php 新文件: assets/opencc/vendor/symfony/process/Exception/InvalidArgumentException.php 新文件: assets/opencc/vendor/symfony/process/Exception/LogicException.php 新文件: assets/opencc/vendor/symfony/process/Exception/ProcessFailedException.php 新文件: assets/opencc/vendor/symfony/process/Exception/ProcessSignaledException.php 新文件: assets/opencc/vendor/symfony/process/Exception/ProcessTimedOutException.php 新文件: assets/opencc/vendor/symfony/process/Exception/RunProcessFailedException.php 新文件: assets/opencc/vendor/symfony/process/Exception/RuntimeException.php 新文件: assets/opencc/vendor/symfony/process/ExecutableFinder.php 新文件: assets/opencc/vendor/symfony/process/InputStream.php 新文件: assets/opencc/vendor/symfony/process/LICENSE 新文件: assets/opencc/vendor/symfony/process/Messenger/RunProcessContext.php 新文件: assets/opencc/vendor/symfony/process/Messenger/RunProcessMessage.php 新文件: assets/opencc/vendor/symfony/process/Messenger/RunProcessMessageHandler.php 新文件: assets/opencc/vendor/symfony/process/PhpExecutableFinder.php 新文件: assets/opencc/vendor/symfony/process/PhpProcess.php 新文件: assets/opencc/vendor/symfony/process/PhpSubprocess.php 新文件: assets/opencc/vendor/symfony/process/Pipes/AbstractPipes.php 新文件: assets/opencc/vendor/symfony/process/Pipes/PipesInterface.php 新文件: assets/opencc/vendor/symfony/process/Pipes/UnixPipes.php 新文件: assets/opencc/vendor/symfony/process/Pipes/WindowsPipes.php 新文件: assets/opencc/vendor/symfony/process/Process.php 新文件: assets/opencc/vendor/symfony/process/ProcessUtils.php 新文件: assets/opencc/vendor/symfony/process/README.md 新文件: assets/opencc/vendor/symfony/process/composer.json 新文件: assets/opencc/vendor/symfony/service-contracts/Attribute/Required.php 新文件: assets/opencc/vendor/symfony/service-contracts/Attribute/SubscribedService.php 新文件: assets/opencc/vendor/symfony/service-contracts/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/service-contracts/LICENSE 新文件: assets/opencc/vendor/symfony/service-contracts/README.md 新文件: assets/opencc/vendor/symfony/service-contracts/ResetInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceCollectionInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceLocatorTrait.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceMethodsSubscriberTrait.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceProviderInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceSubscriberInterface.php 新文件: assets/opencc/vendor/symfony/service-contracts/ServiceSubscriberTrait.php 新文件: assets/opencc/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php 新文件: assets/opencc/vendor/symfony/service-contracts/Test/ServiceLocatorTestCase.php 新文件: assets/opencc/vendor/symfony/service-contracts/composer.json 新文件: assets/opencc/vendor/symfony/string/AbstractString.php 新文件: assets/opencc/vendor/symfony/string/AbstractUnicodeString.php 新文件: assets/opencc/vendor/symfony/string/ByteString.php 新文件: assets/opencc/vendor/symfony/string/CHANGELOG.md 新文件: assets/opencc/vendor/symfony/string/CodePointString.php 新文件: assets/opencc/vendor/symfony/string/Exception/ExceptionInterface.php 新文件: assets/opencc/vendor/symfony/string/Exception/InvalidArgumentException.php 新文件: assets/opencc/vendor/symfony/string/Exception/RuntimeException.php 新文件: assets/opencc/vendor/symfony/string/Inflector/EnglishInflector.php 新文件: assets/opencc/vendor/symfony/string/Inflector/FrenchInflector.php 新文件: assets/opencc/vendor/symfony/string/Inflector/InflectorInterface.php 新文件: assets/opencc/vendor/symfony/string/LICENSE 新文件: assets/opencc/vendor/symfony/string/LazyString.php 新文件: assets/opencc/vendor/symfony/string/README.md 新文件: assets/opencc/vendor/symfony/string/Resources/data/wcswidth_table_wide.php 新文件: assets/opencc/vendor/symfony/string/Resources/data/wcswidth_table_zero.php 新文件: assets/opencc/vendor/symfony/string/Resources/functions.php 新文件: assets/opencc/vendor/symfony/string/Slugger/AsciiSlugger.php 新文件: assets/opencc/vendor/symfony/string/Slugger/SluggerInterface.php 新文件: assets/opencc/vendor/symfony/string/UnicodeString.php 新文件: assets/opencc/vendor/symfony/string/composer.json 新文件: assets/phpliteadmin.php 新文件: config/Caddyfile 新文件: config/nginx.conf 新文件: cron.php 新文件: cron/requirements.txt 新文件: cron/update.py 新文件: index.php 新文件: manage.php 新文件: public.php 新文件: update.php
784 lines
36 KiB
PHP
784 lines
36 KiB
PHP
<?php
|
||
/**
|
||
* @file manage.php
|
||
* @brief 管理页面部分
|
||
*
|
||
* 管理界面脚本,用于处理会话管理、密码更改、登录验证、配置更新、更新日志展示等功能。
|
||
*
|
||
* 作者: Tak
|
||
* GitHub: https://github.com/taksssss/EPG-Server
|
||
* 二次开发: mxdabc
|
||
* Github: https://github.com/mxdabc/epgphp
|
||
*/
|
||
|
||
// 引入公共脚本,初始化数据库
|
||
require_once 'public.php';
|
||
initialDB();
|
||
|
||
session_start();
|
||
|
||
// 首次进入界面,检查 cron.php 是否运行正常
|
||
if ($Config['interval_time'] !== 0) {
|
||
$output = [];
|
||
exec("ps aux | grep '[c]ron.php'", $output);
|
||
if(!$output) {
|
||
exec('php cron.php > /dev/null 2>/dev/null &');
|
||
}
|
||
}
|
||
|
||
// 过渡到新的 md5 密码并生成默认 token、user_agent (如果不存在或为空)
|
||
if (!preg_match('/^[a-f0-9]{32}$/i', $Config['manage_password']) || empty($Config['token']) || empty($Config['user_agent'])) {
|
||
if (!preg_match('/^[a-f0-9]{32}$/i', $Config['manage_password'])) {
|
||
$Config['manage_password'] = md5($Config['manage_password']);
|
||
}
|
||
if (empty($Config['token'])) {
|
||
$Config['token'] = substr(bin2hex(random_bytes(5)), 0, 10); // 生成 10 位随机字符串
|
||
}
|
||
if (empty($Config['user_agent'])) {
|
||
$Config['user_agent'] = substr(bin2hex(random_bytes(5)), 0, 10); // 生成 10 位随机字符串
|
||
}
|
||
file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
// 处理密码更新请求
|
||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['change_password'])) {
|
||
$oldPassword = md5($_POST['old_password']);
|
||
$newPassword = md5($_POST['new_password']);
|
||
|
||
// 验证原密码是否正确
|
||
if ($oldPassword === $Config['manage_password']) {
|
||
// 原密码正确,更新配置中的密码
|
||
$Config['manage_password'] = $newPassword;
|
||
|
||
// 将新配置写回 config.json
|
||
file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
|
||
// 设置密码更改成功的标志变量
|
||
$passwordChanged = true;
|
||
} else {
|
||
$passwordChangeError = "原密码错误";
|
||
}
|
||
}
|
||
|
||
// 检查是否提交登录表单
|
||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['login'])) {
|
||
$password = md5($_POST['password']);
|
||
|
||
// 验证密码
|
||
if ($password === $Config['manage_password']) {
|
||
// 密码正确,设置会话变量
|
||
$_SESSION['loggedin'] = true;
|
||
|
||
// 设置会话变量,表明用户可以访问 phpliteadmin.php 、 tinyfilemanager.php
|
||
$_SESSION['can_access_phpliteadmin'] = true;
|
||
$_SESSION['can_access_tinyfilemanager'] = true;
|
||
} else {
|
||
$error = "密码错误";
|
||
}
|
||
}
|
||
|
||
// 处理密码更改成功后的提示
|
||
$passwordChangedMessage = isset($passwordChanged) ? "<p style='color:green;'>密码已更改</p>" : '';
|
||
$passwordChangeErrorMessage = isset($passwordChangeError) ? "<p style='color:red;'>$passwordChangeError</p>" : '';
|
||
|
||
// 检查是否已登录
|
||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||
// 显示登录表单
|
||
include 'assets/html/login.html';
|
||
exit;
|
||
}
|
||
|
||
// 更新配置
|
||
function updateConfigFields() {
|
||
global $Config, $configPath;
|
||
|
||
// 获取和过滤表单数据
|
||
$config_keys = array_keys(array_filter($_POST, function($key) {
|
||
return $key !== 'update_config';
|
||
}, ARRAY_FILTER_USE_KEY));
|
||
|
||
foreach ($config_keys as $key) {
|
||
${$key} = is_numeric($_POST[$key]) ? intval($_POST[$key]) : $_POST[$key];
|
||
}
|
||
|
||
// 处理 URL 列表和频道别名
|
||
$xml_urls = array_values(array_map(function($url) {
|
||
return preg_replace('/^#\s*(\S+)(\s*#.*)?$/', '# $1$2', trim(str_replace([",", ":"], [",", ":"], $url)));
|
||
}, explode("\n", $xml_urls)));
|
||
|
||
$interval_time = $interval_hour * 3600 + $interval_minute * 60;
|
||
$mysql = ["host" => $mysql_host, "dbname" => $mysql_dbname, "username" => $mysql_username, "password" => $mysql_password];
|
||
|
||
// 解析频道别名
|
||
$channel_mappings = [];
|
||
if ($mappings = trim($_POST['channel_mappings'] ?? '')) {
|
||
foreach (explode("\n", $mappings) as $line) {
|
||
if ($line = trim($line)) {
|
||
list($search, $replace) = preg_split('/=》|=>/', $line);
|
||
$channel_mappings[trim($search)] = trim(str_replace(",", ",", trim($replace)), '[]');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 解析频道 EPG 数据
|
||
$channel_bind_epg = isset($_POST['channel_bind_epg']) ? array_filter(array_reduce(json_decode($_POST['channel_bind_epg'], true), function($result, $item) {
|
||
$epgSrc = preg_replace('/^【已停用】/', '', $item['epg_src']);
|
||
if (!empty($item['channels'])) $result[$epgSrc] = trim(str_replace(",", ",", trim($item['channels'])), '[]');
|
||
return $result;
|
||
}, [])) : $Config['channel_bind_epg'];
|
||
|
||
// 更新 $Config
|
||
$oldConfig = $Config;
|
||
$config_keys_filtered = array_filter($config_keys, function($key) {
|
||
return !preg_match('/^(mysql_|interval_)/', $key);
|
||
});
|
||
$config_keys_new = ['channel_bind_epg', 'interval_time', 'mysql'];
|
||
$config_keys_save = array_merge($config_keys_filtered, $config_keys_new);
|
||
|
||
foreach ($config_keys_save as $key) {
|
||
if (isset($$key)) {
|
||
$Config[$key] = $$key;
|
||
}
|
||
}
|
||
|
||
// 检查 MySQL 有效性
|
||
$db_type_set = true;
|
||
if ($Config['db_type'] === 'mysql') {
|
||
try {
|
||
$dsn = "mysql:host={$Config['mysql']['host']};dbname={$Config['mysql']['dbname']};charset=utf8mb4";
|
||
$db = new PDO($dsn, $Config['mysql']['username'] ?? null, $Config['mysql']['password'] ?? null);
|
||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
} catch (PDOException $e) {
|
||
$Config['db_type'] = 'sqlite';
|
||
$db_type_set = false;
|
||
}
|
||
}
|
||
|
||
// 将新配置写回 config.json
|
||
file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
|
||
// 重新启动 cron.php ,设置新的定时任务
|
||
if ($oldConfig['start_time'] !== $start_time || $oldConfig['end_time'] !== $end_time || $oldConfig['interval_time'] !== $interval_time) {
|
||
exec('php cron.php > /dev/null 2>/dev/null &');
|
||
}
|
||
|
||
return ['db_type_set' => $db_type_set];
|
||
}
|
||
|
||
// 处理服务器请求
|
||
try {
|
||
$requestMethod = $_SERVER['REQUEST_METHOD'];
|
||
$dbResponse = null;
|
||
|
||
if ($requestMethod == 'GET') {
|
||
|
||
// 确定操作类型
|
||
$action_map = [
|
||
'get_update_logs', 'get_cron_logs', 'get_channel', 'get_epg_by_channel',
|
||
'get_icon', 'get_channel_bind_epg', 'get_channel_match', 'get_gen_list',
|
||
'get_live_data', 'parse_source_info', 'toggle_status',
|
||
'download_data', 'delete_unused_icons', 'delete_unused_live_data',
|
||
'get_version_log'
|
||
];
|
||
$action = key(array_intersect_key($_GET, array_flip($action_map))) ?: '';
|
||
|
||
// 根据操作类型执行不同的逻辑
|
||
switch ($action) {
|
||
case 'get_update_logs':
|
||
// 获取更新日志
|
||
$dbResponse = $db->query("SELECT * FROM update_log")->fetchAll(PDO::FETCH_ASSOC);
|
||
break;
|
||
|
||
case 'get_cron_logs':
|
||
// 获取 cron 日志
|
||
$dbResponse = $db->query("SELECT * FROM cron_log")->fetchAll(PDO::FETCH_ASSOC);
|
||
break;
|
||
|
||
case 'get_channel':
|
||
// 获取频道
|
||
$channels = $db->query("SELECT DISTINCT channel FROM epg_data ORDER BY channel ASC")->fetchAll(PDO::FETCH_COLUMN);
|
||
$channelMappings = $Config['channel_mappings'];
|
||
$mappedChannels = [];
|
||
foreach ($channelMappings as $mapped => $original) {
|
||
if (($index = array_search(strtoupper($mapped), $channels)) !== false) {
|
||
$mappedChannels[] = [
|
||
'original' => $mapped,
|
||
'mapped' => $original
|
||
];
|
||
unset($channels[$index]); // 从剩余频道中移除
|
||
}
|
||
}
|
||
foreach ($channels as $channel) {
|
||
$mappedChannels[] = [
|
||
'original' => $channel,
|
||
'mapped' => ''
|
||
];
|
||
}
|
||
$dbResponse = [
|
||
'channels' => $mappedChannels,
|
||
'count' => count($mappedChannels)
|
||
];
|
||
break;
|
||
|
||
case 'get_epg_by_channel':
|
||
// 查询
|
||
$channel = $_GET['channel'];
|
||
$date = urldecode($_GET['date']);
|
||
$stmt = $db->prepare("SELECT epg_diyp FROM epg_data WHERE channel = :channel AND date = :date");
|
||
$stmt->execute([':channel' => $channel, ':date' => $date]);
|
||
$result = $stmt->fetch(PDO::FETCH_ASSOC); // 获取单条结果
|
||
if ($result) {
|
||
$epgData = json_decode($result['epg_diyp'], true);
|
||
$epgSource = $epgData['source'] ?? '';
|
||
$epgOutput = "";
|
||
foreach ($epgData['epg_data'] as $epgItem) {
|
||
$epgOutput .= "{$epgItem['start']} {$epgItem['title']}\n";
|
||
}
|
||
$dbResponse = ['channel' => $channel, 'source' => $epgSource, 'date' => $date, 'epg' => trim($epgOutput)];
|
||
} else {
|
||
$dbResponse = ['channel' => $channel, 'source' => '', 'date' => $date, 'epg' => '无节目信息'];
|
||
}
|
||
break;
|
||
|
||
case 'get_icon':
|
||
// 是否显示无节目单的内置台标
|
||
if(isset($_GET['get_all_icon'])) {
|
||
$iconList = $iconListMerged;
|
||
}
|
||
|
||
// 获取并合并数据库中的频道和 $iconList 中的频道,去重后按字母排序
|
||
$allChannels = array_unique(array_merge(
|
||
$db->query("SELECT DISTINCT channel FROM epg_data ORDER BY channel ASC")->fetchAll(PDO::FETCH_COLUMN),
|
||
array_keys($iconList)
|
||
));
|
||
sort($allChannels);
|
||
|
||
// 将默认台标插入到频道列表的开头
|
||
$defaultIcon = [
|
||
['channel' => '【默认台标】', 'icon' => $Config['default_icon'] ?? '']
|
||
];
|
||
|
||
$channelsInfo = array_map(function($channel) use ($iconList) {
|
||
return ['channel' => $channel, 'icon' => $iconList[$channel] ?? ''];
|
||
}, $allChannels);
|
||
$withIcons = array_filter($channelsInfo, function($c) { return !empty($c['icon']);});
|
||
$withoutIcons = array_filter($channelsInfo, function($c) { return empty($c['icon']);});
|
||
|
||
$dbResponse = [
|
||
'channels' => array_merge($defaultIcon, $withIcons, $withoutIcons),
|
||
'count' => count($allChannels)
|
||
];
|
||
break;
|
||
|
||
case 'get_channel_bind_epg':
|
||
// 获取频道绑定的 EPG
|
||
$channels = $db->query("SELECT DISTINCT channel FROM epg_data ORDER BY channel ASC")->fetchAll(PDO::FETCH_COLUMN);
|
||
$channelBindEpg = $Config['channel_bind_epg'] ?? [];
|
||
$xmlUrls = $Config['xml_urls'];
|
||
$dbResponse = array_map(function($epgSrc) use ($channelBindEpg) {
|
||
$cleanEpgSrc = trim(explode('#', strpos($epgSrc, '=>') !== false ? explode('=>', $epgSrc)[1] : ltrim($epgSrc, '# '))[0]);
|
||
$isInactive = strpos(trim($epgSrc), '#') === 0;
|
||
return [
|
||
'epg_src' => ($isInactive ? '【已停用】' : '') . $cleanEpgSrc,
|
||
'channels' => $channelBindEpg[$cleanEpgSrc] ?? ''
|
||
];
|
||
}, array_filter($xmlUrls, function($epgSrc) {
|
||
// 去除空行和包含 tvmao、cntv 的行
|
||
return !empty(ltrim($epgSrc, '# ')) && strpos($epgSrc, 'tvmao') === false && strpos($epgSrc, 'cntv') === false;
|
||
}));
|
||
$dbResponse = array_merge(
|
||
array_filter($dbResponse, function($item) { return strpos($item['epg_src'], '【已停用】') === false; }),
|
||
array_filter($dbResponse, function($item) { return strpos($item['epg_src'], '【已停用】') !== false; })
|
||
);
|
||
break;
|
||
|
||
case 'get_channel_match':
|
||
// 获取频道匹配
|
||
$channels = $db->query("SELECT channel FROM gen_list")->fetchAll(PDO::FETCH_COLUMN);
|
||
if (empty($channels)) {
|
||
echo json_encode(['ori_channels' => [], 'clean_channels' => [], 'match' => [], 'type' => []]);
|
||
exit;
|
||
}
|
||
$cleanChannels = explode("\n", t2s(implode("\n", array_map('cleanChannelName', $channels))));
|
||
$epgData = $db->query("SELECT channel FROM epg_data")->fetchAll(PDO::FETCH_COLUMN);
|
||
$channelMap = array_combine($cleanChannels, $channels);
|
||
$matches = [];
|
||
foreach ($cleanChannels as $cleanChannel) {
|
||
$originalChannel = $channelMap[$cleanChannel];
|
||
$matchResult = null;
|
||
$matchType = '未匹配';
|
||
if (in_array($cleanChannel, $epgData)) {
|
||
$matchResult = $cleanChannel;
|
||
$matchType = '精确匹配';
|
||
if ($cleanChannel !== $originalChannel) {
|
||
$matchType = '别名/忽略';
|
||
}
|
||
} else {
|
||
foreach ($epgData as $epgChannel) {
|
||
if (stripos($epgChannel, $cleanChannel) !== false) {
|
||
if (!isset($matchResult) || strlen($epgChannel) < strlen($matchResult)) {
|
||
$matchResult = $epgChannel;
|
||
$matchType = '正向模糊';
|
||
}
|
||
} elseif (stripos($cleanChannel, $epgChannel) !== false) {
|
||
if (!isset($matchResult) || strlen($epgChannel) > strlen($matchResult)) {
|
||
$matchResult = $epgChannel;
|
||
$matchType = '反向模糊';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
$matches[$cleanChannel] = [
|
||
'ori_channel' => $originalChannel,
|
||
'clean_channel' => $cleanChannel,
|
||
'match' => $matchResult,
|
||
'type' => $matchType
|
||
];
|
||
}
|
||
$dbResponse = $matches;
|
||
break;
|
||
|
||
case 'get_gen_list':
|
||
// 获取生成列表
|
||
$dbResponse = $db->query("SELECT channel FROM gen_list")->fetchAll(PDO::FETCH_COLUMN);
|
||
break;
|
||
|
||
case 'get_live_data':
|
||
// 读取文件内容
|
||
function readFileContent($filePath) {
|
||
return file_exists($filePath) ? file_get_contents($filePath) : '';
|
||
}
|
||
|
||
$sourceContent = readFileContent($liveDir . 'source.txt');
|
||
$templateContent = readFileContent($liveDir . 'template.txt');
|
||
|
||
// 读取 CSV 文件并返回关联数组
|
||
function readCsvFile($filePath, $key = null) {
|
||
if (!file_exists($filePath)) return [];
|
||
|
||
$data = [];
|
||
if (($file = fopen($filePath, 'r')) !== false) {
|
||
$header = fgetcsv($file);
|
||
while (($row = fgetcsv($file)) !== false) {
|
||
if (empty(array_filter($row)) || count($row) !== count($header)) continue;
|
||
|
||
$rowData = array_combine($header, $row);
|
||
if ($key && isset($rowData[$key])) {
|
||
$data[$rowData[$key]] = $rowData; // 使用指定键映射
|
||
} else {
|
||
$data[] = $rowData;
|
||
}
|
||
}
|
||
fclose($file);
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
$channelsInfo = readCsvFile($liveDir . 'channels_info.csv', 'tag');
|
||
$channelsData = readCsvFile($liveDir . 'channels.csv');
|
||
|
||
// 更新 channelsData 中的 resolution 和 speed
|
||
foreach ($channelsData as &$row) {
|
||
if (isset($channelsInfo[$row['tag']])) {
|
||
$row['resolution'] = str_replace("x", "<br>x<br>", $channelsInfo[$row['tag']]['resolution']);
|
||
$row['speed'] = $channelsInfo[$row['tag']]['speed'];
|
||
if (is_numeric($row['speed'])) { $row['speed'] .= '<br>ms';}
|
||
}
|
||
}
|
||
|
||
generateLiveFiles($channelsData, 'tv', $saveOnly = true); // 重新生成 M3U 和 TXT 文件
|
||
|
||
$dbResponse = ['source_content' => $sourceContent, 'template_content' => $templateContent, 'channels' => $channelsData,];
|
||
break;
|
||
|
||
case 'parse_source_info':
|
||
// 解析直播源
|
||
$parseResult = doParseSourceInfo();
|
||
if ($parseResult !== true) {
|
||
$dbResponse = ['success' => 'part', 'message' => $parseResult];
|
||
} else {
|
||
$dbResponse = ['success' => 'full'];
|
||
}
|
||
break;
|
||
|
||
case 'toggle_status':
|
||
// 切换状态
|
||
$toggleField = $_GET['toggle_button'] === 'toggleLiveSourceSyncBtn' ? 'live_source_auto_sync'
|
||
: ($_GET['toggle_button'] === 'toggleLiveChannelNameProcessBtn' ? 'live_channel_name_process' : '');
|
||
$currentStatus = isset($Config[$toggleField]) && $Config[$toggleField] == 1 ? 1 : 0;
|
||
$newStatus = ($currentStatus == 1) ? 0 : 1;
|
||
$Config[$toggleField] = $newStatus;
|
||
file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
|
||
$dbResponse = ['status' => $newStatus];
|
||
break;
|
||
|
||
case 'download_data':
|
||
// 下载数据
|
||
$url = filter_var(($_GET['url']), FILTER_VALIDATE_URL);
|
||
if ($url) {
|
||
$data = downloadData($url, '', 5);
|
||
if ($data !== false) {
|
||
$dbResponse = ['success' => true, 'data' => $data];
|
||
} else {
|
||
$dbResponse = ['success' => false, 'message' => '无法获取URL内容'];
|
||
}
|
||
} else {
|
||
$dbResponse = ['success' => false, 'message' => '无效的URL'];
|
||
}
|
||
break;
|
||
|
||
case 'delete_unused_icons':
|
||
// 清理未在使用的台标
|
||
$iconUrls = array_merge(
|
||
array_map(function($url) { return parse_url($url, PHP_URL_PATH); }, $iconList),
|
||
[parse_url($Config["default_icon"], PHP_URL_PATH)]
|
||
);
|
||
$iconPath = __DIR__ . '/data/icon';
|
||
$deletedCount = 0;
|
||
foreach (scandir($iconPath) as $file) {
|
||
if ($file === '.' || $file === '..') continue;
|
||
$iconRltPath = '/data/icon/' . $file;
|
||
if (!in_array($iconRltPath, $iconUrls)) {
|
||
if (@unlink($iconPath . '/' . $file)) {
|
||
$deletedCount++;
|
||
}
|
||
}
|
||
}
|
||
$dbResponse = ['success' => true, 'message' => "共清理了 $deletedCount 个台标"];
|
||
break;
|
||
|
||
case 'delete_unused_live_data':
|
||
// 清理未在使用的直播源缓存、未出现在频道列表中的修改记录
|
||
$sourceFilePath = $liveDir . 'source.txt';
|
||
$sourceContent = file_exists($sourceFilePath) ? file_get_contents($sourceFilePath) : '';
|
||
$urls = array_map('trim', explode("\n", $sourceContent));
|
||
|
||
// 遍历 live/file 目录,删除未使用的文件
|
||
$parentRltPath = '/' . basename(__DIR__) . '/data/live/file/'; // 相对路径
|
||
$deletedFileCount = 0;
|
||
foreach (scandir($liveFileDir) as $file) {
|
||
if ($file === '.' || $file === '..') continue;
|
||
$fileRltPath = $parentRltPath . $file;
|
||
if (!array_filter($urls, function($url) use ($fileRltPath) {
|
||
$url = trim(explode('#', ltrim($url, '# '))[0]); // 处理注释
|
||
$urlmd5 = md5(urlencode($url)); // 计算 md5
|
||
return $url && (stripos($fileRltPath, $url) !== false || stripos($fileRltPath, $urlmd5) !== false);
|
||
})) {
|
||
if (@unlink($liveFileDir . $file)) { // 如果没有匹配的 URL,删除文件
|
||
$deletedFileCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除 modifications.csv 未在 channels.csv 中出现的条目
|
||
$channelsFilePath = $liveDir . 'channels.csv';
|
||
$modificationsFilePath = $liveDir . 'modifications.csv';
|
||
|
||
$deletedRecordCount = 0;
|
||
if (file_exists($channelsFilePath) && file_exists($modificationsFilePath)) {
|
||
// 读取 channels.csv 中的 tag 字段
|
||
$channelTags = [];
|
||
$file = fopen($channelsFilePath, 'r');
|
||
$header = fgetcsv($file);
|
||
while (($row = fgetcsv($file)) !== false) {
|
||
$channelTags[] = $row[array_search('tag', $header)];
|
||
}
|
||
fclose($file);
|
||
|
||
// 过滤 modifications.csv 数据并统计移除行数
|
||
$file = fopen($modificationsFilePath, 'r');
|
||
$modificationsHeader = fgetcsv($file);
|
||
$filteredData = [];
|
||
while (($row = fgetcsv($file)) !== false) {
|
||
if (in_array($row[array_search('tag', $modificationsHeader)], $channelTags)) {
|
||
$filteredData[] = $row;
|
||
} else {
|
||
$deletedRecordCount++;
|
||
}
|
||
}
|
||
fclose($file);
|
||
|
||
// 写回过滤后的数据
|
||
$file = fopen($modificationsFilePath, 'w');
|
||
fputcsv($file, $modificationsHeader);
|
||
foreach ($filteredData as $row) {
|
||
fputcsv($file, $row);
|
||
}
|
||
fclose($file);
|
||
}
|
||
|
||
$dbResponse = ['success' => true, 'message' => "共清理了 $deletedFileCount 个缓存文件, $deletedRecordCount 条修改记录。"];
|
||
break;
|
||
|
||
case 'get_version_log':
|
||
// 获取更新日志
|
||
$checkUpdateEnable = !isset($Config['check_update']) || $Config['check_update'] == 1;
|
||
$checkUpdate = isset($_GET['do_check_update']) && $_GET['do_check_update'] === 'true';
|
||
if (!$checkUpdateEnable && $checkUpdate) {
|
||
echo json_encode(['success' => true, 'is_updated' => false]);
|
||
return;
|
||
}
|
||
|
||
$localFile = 'assets/CHANGELOG.md';
|
||
$url = 'https://gitee.com/taksssss/EPG-Server/raw/main/CHANGELOG.md';
|
||
$isUpdated = false;
|
||
$updateMessage = '';
|
||
if ($checkUpdate) {
|
||
$remoteContent = @file_get_contents($url);
|
||
if ($remoteContent === false) {
|
||
echo json_encode(['success' => false, 'message' => '无法获取远程版本日志']);
|
||
return;
|
||
}
|
||
$localContent = file_exists($localFile) ? file_get_contents($localFile) : '';
|
||
if (strtok($localContent, "\n") !== strtok($remoteContent, "\n")) {
|
||
file_put_contents($localFile, $remoteContent);
|
||
$isUpdated = !empty($localContent) ? true : false;
|
||
$updateMessage = '<h3 style="color: red;">🔔 检测到新版本,请自行更新。(该提醒仅显示一次)</h3>';
|
||
}
|
||
}
|
||
|
||
$markdownContent = file_exists($localFile) ? file_get_contents($localFile) : false;
|
||
if ($markdownContent === false) {
|
||
echo json_encode(['success' => false, 'message' => '无法读取版本日志']);
|
||
return;
|
||
}
|
||
|
||
require_once 'assets/Parsedown.php';
|
||
$htmlContent = (new Parsedown())->text($markdownContent);
|
||
$dbResponse = ['success' => true, 'content' => $updateMessage . $htmlContent, 'is_updated' => $isUpdated];
|
||
break;
|
||
|
||
default:
|
||
$dbResponse = null;
|
||
break;
|
||
}
|
||
|
||
if ($dbResponse !== null) {
|
||
header('Content-Type: application/json');
|
||
echo json_encode($dbResponse);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// 处理 POST 请求
|
||
if ($requestMethod === 'POST') {
|
||
// 定义操作类型和对应的条件
|
||
$actions = [
|
||
'update_config' => isset($_POST['update_config']),
|
||
'set_gen_list' => isset($_GET['set_gen_list']),
|
||
'import_config' => isset($_POST['importExport']) && !empty($_FILES['importFile']['tmp_name']),
|
||
'export_config' => isset($_POST['importExport']) && empty($_FILES['importFile']['tmp_name']),
|
||
'upload_icon' => isset($_FILES['iconFile']),
|
||
'update_icon_list' => isset($_POST['update_icon_list']),
|
||
'upload_source_file' => isset($_FILES['liveSourceFile']),
|
||
'save_content_to_file' => isset($_POST['save_content_to_file']),
|
||
'save_source_info' => isset($_POST['save_source_info']),
|
||
'update_config_field' => isset($_POST['update_config_field']),
|
||
];
|
||
|
||
// 确定操作类型
|
||
$action = '';
|
||
foreach ($actions as $key => $condition) {
|
||
if ($condition) { $action = $key; break; }
|
||
}
|
||
|
||
switch ($action) {
|
||
case 'update_config':
|
||
// 更新配置
|
||
['db_type_set' => $db_type_set] = updateConfigFields();
|
||
echo json_encode([
|
||
'db_type_set' => $db_type_set,
|
||
'interval_time' => $Config['interval_time'],
|
||
'start_time' => $Config['start_time'],
|
||
'end_time' => $Config['end_time']
|
||
]);
|
||
exit;
|
||
|
||
case 'set_gen_list':
|
||
// 设置生成列表
|
||
$data = json_decode(file_get_contents("php://input"), true)['data'] ?? '';
|
||
try {
|
||
$db->beginTransaction();
|
||
$db->exec("DELETE FROM gen_list");
|
||
$lines = array_filter(array_map('trim', explode("\n", $data)));
|
||
foreach ($lines as $line) {
|
||
$stmt = $db->prepare("INSERT INTO gen_list (channel) VALUES (:channel)");
|
||
$stmt->bindValue(':channel', $line, PDO::PARAM_STR);
|
||
$stmt->execute();
|
||
}
|
||
$db->commit();
|
||
echo 'success';
|
||
} catch (PDOException $e) {
|
||
$db->rollBack();
|
||
echo "数据库操作失败: " . $e->getMessage();
|
||
}
|
||
exit;
|
||
|
||
case 'import_config':
|
||
// 导入配置
|
||
$zip = new ZipArchive();
|
||
$importFile = $_FILES['importFile']['tmp_name'];
|
||
$successFlag = false;
|
||
$message = "";
|
||
if ($zip->open($importFile) === TRUE) {
|
||
if ($zip->extractTo('.')) {
|
||
$successFlag = true;
|
||
$message = "导入成功!<br>3秒后自动刷新页面……";
|
||
} else {
|
||
$message = "导入失败!解压过程中发生问题。";
|
||
}
|
||
$zip->close();
|
||
} else {
|
||
$message = "导入失败!无法打开压缩文件。";
|
||
}
|
||
echo json_encode(['success' => $successFlag, 'message' => $message]);
|
||
exit;
|
||
|
||
case 'export_config':
|
||
$zip = new ZipArchive();
|
||
$zipFileName = 't.gz';
|
||
if ($zip->open($zipFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
|
||
$dataDir = __DIR__ . '/data';
|
||
$files = new RecursiveIteratorIterator(
|
||
new RecursiveDirectoryIterator($dataDir),
|
||
RecursiveIteratorIterator::LEAVES_ONLY
|
||
);
|
||
foreach ($files as $file) {
|
||
if (!$file->isDir()) {
|
||
$filePath = $file->getRealPath();
|
||
$relativePath = 'data/' . substr($filePath, strlen($dataDir) + 1);
|
||
$zip->addFile($filePath, $relativePath);
|
||
}
|
||
}
|
||
$zip->close();
|
||
header('Content-Type: application/zip');
|
||
header('Content-Disposition: attachment; filename=' . $zipFileName);
|
||
readfile($zipFileName);
|
||
unlink($zipFileName);
|
||
}
|
||
exit;
|
||
|
||
case 'upload_icon':
|
||
// 上传图标
|
||
$file = $_FILES['iconFile'];
|
||
$fileName = $file['name'];
|
||
$uploadFile = $iconDir . $fileName;
|
||
if ($file['type'] === 'image/png' && move_uploaded_file($file['tmp_name'], $uploadFile)) {
|
||
$iconUrl = $serverUrl . '/data/icon/' . basename($fileName);
|
||
echo json_encode(['success' => true, 'iconUrl' => $iconUrl]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'message' => '文件上传失败']);
|
||
}
|
||
exit;
|
||
|
||
case 'update_icon_list':
|
||
// 更新图标
|
||
$iconList = [];
|
||
$updatedIcons = json_decode($_POST['updatedIcons'], true);
|
||
|
||
// 遍历更新数据
|
||
foreach ($updatedIcons as $channelData) {
|
||
$channelName = strtoupper(trim($channelData['channel']));
|
||
if ($channelName === '【默认台标】') {
|
||
// 保存默认台标到 config.json
|
||
$Config['default_icon'] = $channelData['icon'] ?? '';
|
||
file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
} else {
|
||
// 处理普通台标数据
|
||
$iconList[$channelName] = $channelData['icon'];
|
||
}
|
||
}
|
||
|
||
// 过滤掉图标值为空和频道名为空的条目
|
||
$iconList = array_filter($iconList, function($icon, $channel) {
|
||
return !empty($icon) && !empty($channel);
|
||
}, ARRAY_FILTER_USE_BOTH);
|
||
|
||
if (file_put_contents($iconListPath, json_encode($iconList, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) === false) {
|
||
echo json_encode(['success' => false, 'message' => '更新 iconList.json 时发生错误']);
|
||
} else {
|
||
echo json_encode(['success' => true]);
|
||
}
|
||
exit;
|
||
|
||
case 'upload_source_file':
|
||
// 上传直播源文件
|
||
$file = $_FILES['liveSourceFile'];
|
||
$fileName = $file['name'];
|
||
$uploadFile = $liveFileDir . $fileName;
|
||
if (move_uploaded_file($file['tmp_name'], $uploadFile)) {
|
||
$liveSourceUrl = '/data/live/file/' . basename($fileName);
|
||
$sourceFilePath = $liveDir . 'source.txt';
|
||
$currentContent = file_get_contents($sourceFilePath);
|
||
if (!file_exists($sourceFilePath) || strpos($currentContent, $liveSourceUrl) === false) {
|
||
// 如果文件不存在或文件中没有该 URL,将其追加到文件末尾
|
||
$contentToAppend = trim($currentContent) ? PHP_EOL . $liveSourceUrl : $liveSourceUrl;
|
||
file_put_contents($sourceFilePath, $contentToAppend, FILE_APPEND);
|
||
}
|
||
echo json_encode(['success' => true]);
|
||
} else {
|
||
echo json_encode(['success' => false, 'message' => '文件上传失败']);
|
||
}
|
||
exit;
|
||
|
||
case 'save_content_to_file':
|
||
// 保存内容到文件
|
||
$filePath = __DIR__ . $_POST['file_path'] ?? '';
|
||
$content = $_POST['content'] ?? '';
|
||
if (file_put_contents($filePath, str_replace(",", ",", $content)) !== false) {
|
||
echo json_encode(['success' => true]);
|
||
} else {
|
||
http_response_code(500);
|
||
echo json_encode(['success' => false]);
|
||
}
|
||
exit;
|
||
|
||
case 'save_source_info':
|
||
// 更新配置文件
|
||
$Config['live_tvg_logo_enable'] = (int)$_POST['live_tvg_logo_enable'];
|
||
$Config['live_tvg_id_enable'] = (int)$_POST['live_tvg_id_enable'];
|
||
$Config['live_tvg_name_enable'] = (int)$_POST['live_tvg_name_enable'];
|
||
|
||
if (file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) === false) {
|
||
http_response_code(500);
|
||
echo json_encode(['success' => false, 'message' => '保存配置文件失败']);
|
||
exit;
|
||
}
|
||
|
||
// 保存直播源信息
|
||
$content = json_decode($_POST['content'], true);
|
||
if (empty($content)) {
|
||
echo json_encode(['success' => false, 'message' => '无效的数据']);
|
||
exit;
|
||
}
|
||
|
||
$fileName = $_POST['file_path'] ? 'file/' . md5(urlencode($_POST['file_path'])) : 'tv';
|
||
generateLiveFiles($content, $fileName, $saveOnly = true); // 重新生成 M3U 和 TXT 文件
|
||
echo json_encode(['success' => true]);
|
||
exit;
|
||
|
||
case 'update_config_field':
|
||
// 更新单个字段
|
||
foreach ($_POST as $key => $value) {
|
||
// 排除 update_config_field 字段
|
||
if ($key !== 'update_config_field') {
|
||
$Config[$key] = is_numeric($value) ? intval($value) : $value;
|
||
}
|
||
}
|
||
if (file_put_contents($configPath, json_encode($Config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) !== false) {
|
||
echo json_encode(['success' => $Config]);
|
||
} else {
|
||
http_response_code(500);
|
||
echo '保存失败';
|
||
}
|
||
exit;
|
||
}
|
||
}
|
||
} catch (Exception $e) {
|
||
// 处理数据库连接错误
|
||
}
|
||
|
||
// 生成配置管理表单
|
||
include 'assets/html/manage.html';
|
||
?>
|