This commit is contained in:
2026-03-27 07:06:16 +01:00
commit 1541961403
340 changed files with 151916 additions and 0 deletions
+491
View File
@@ -0,0 +1,491 @@
-- modified from https://github.com/idiomic/Lua_AES
--[[
Copyright 2019 Tyler Richard Hoyer
Copyright 2025 dyphire
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local unpack = unpack or table.unpack
local GF8x2 = {
[0]=0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,
0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,
0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,
0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,
0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,
0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,
0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,
0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,
0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,
0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,
0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,
0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,
0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,
0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,
0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,
0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5
}
local GF8x3 = {
[0]=0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,
0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,
0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,
0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,
0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,
0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,
0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,
0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,
0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,
0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,
0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,
0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,
0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,
0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,
0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,
0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a
}
local GF8x9 = {
[0]=0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,
0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,
0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,
0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,
0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,
0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,
0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,
0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,
0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,
0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,
0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,
0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,
0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,
0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,
0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,
0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46
}
local GF8x11 = {
[0]=0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,
0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,
0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,
0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,
0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,
0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,
0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,
0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,
0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,
0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,
0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,
0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,
0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,
0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,
0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,
0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3
}
local GF8x13 = {
[0]=0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,
0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,
0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,
0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,
0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,
0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,
0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,
0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,
0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,
0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,
0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,
0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,
0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,
0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,
0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,
0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97
}
local GF8x14 = {
[0]=0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,
0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,
0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,
0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,
0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,
0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,
0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,
0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,
0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,
0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,
0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,
0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,
0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,
0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,
0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,
0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d
}
local s = {
[0]=0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,
0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,
0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,
0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,
0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,
0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,
0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,
0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,
0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,
0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,
0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,
0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,
0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,
0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,
0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,
0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
}
local si = {
[0]=0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
}
local rcon = {
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,
0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,
0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,
0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,
0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,
0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,
0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,
0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,
0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,
0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,
0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,
0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,
0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,
0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,
0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,
0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d
}
local xor4 = {
[0]=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,
2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,
3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,
4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,
5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,
6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,
7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,
8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,
10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,
11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,
12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,
13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,
14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,
15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
}
local function xor8(a, b)
local al = a % 16
local bl = b % 16
return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl]
end
local function xor_blocks_8(a, b)
local res = {}
for i = 1, 16 do
res[i] = xor8(a[i], b[i])
end
return res
end
local function addRoundKey(state, key)
for i, byte in next, state do
state[i] = xor8(byte, key[i])
end
end
local function subBytes(state, s_box)
for i, byte in next, state do
state[i] = s_box[byte]
end
end
local function shiftRows(state)
state[2], state[6], state[10], state[14] =
state[6], state[10], state[14], state[2]
state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] =
state[16], state[4], state[8], state[12]
end
local function inv_shiftRows(state)
state[2], state[6], state[10], state[14] =
state[14], state[2], state[6], state[10]
state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] =
state[8], state[12], state[16], state[4]
end
local function mixColumns(state)
for i = 0, 3 do
local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
state[cur + 0] = xor8(xor8(xor8(GF8x2[a], GF8x3[b]), c), d)
state[cur + 1] = xor8(xor8(xor8(a, GF8x2[b]), GF8x3[c]), d)
state[cur + 2] = xor8(xor8(xor8(a, b), GF8x2[c]), GF8x3[d])
state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d])
end
end
local function inv_mixColumns(state) -- TODO: fix
for i = 0, 3 do
local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
state[cur + 0] = xor8(xor8(xor8(GF8x14[a], GF8x11[b]), GF8x13[c]), GF8x9[d])
state[cur + 1] = xor8(xor8(xor8(GF8x9[a], GF8x14[b]), GF8x11[c]), GF8x13[d])
state[cur + 2] = xor8(xor8(xor8(GF8x13[a], GF8x9[b]), GF8x14[c]), GF8x11[d])
state[cur + 3] = xor8(xor8(xor8(GF8x11[a], GF8x13[b]), GF8x9[c]), GF8x14[d])
end
end
-- 256-bit key constants
local n = 32 -- number of bytes in the 256-bit encryption key
local b = 240 -- number of bytes in 15 128-bit round keys
local function schedule256(key)
local expanded = {}
for c = 0, n-1 do
expanded[c] = key[c]
end
local i = 1
local c = n
local t1, t2, t3, t4 --t
while c < b do
t1 = expanded[c-4]
t2 = expanded[c-3]
t3 = expanded[c-2]
t4 = expanded[c-1]
if (c % n == 0) then
t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1]
i = i + 1
end
if (c % n == 16) then
t1 = s[t1]
t2 = s[t2]
t3 = s[t3]
t4 = s[t4]
end
t1 = xor8(t1, expanded[c - n])
expanded[c] = t1
c = c + 1
t2 = xor8(t2, expanded[c - n])
expanded[c] = t2
c = c + 1
t3 = xor8(t3, expanded[c - n])
expanded[c] = t3
c = c + 1
t4 = xor8(t4, expanded[c - n])
expanded[c] = t4
c = c + 1
end
local roundKeys = {}
for round = 0, 14 do
local roundKey = {}
for byte = 0, 15 do
roundKey[byte+1] = expanded[round * 16 + byte]
end
roundKeys[round] = roundKey
end
return roundKeys
end
local function chunks(text, i)
local first = i * 16 + 1
if first > #text then
return
end
i = i + 1
local chunk = {text:byte(first, first + 15)}
for j = #chunk + 1, 16 do
chunk[j] = 0
end
return i, chunk
end
local function pkcs7_unpad(str)
local len = #str
if len == 0 then return str end
local pad_len = string.byte(str, len)
if pad_len < 1 or pad_len > 16 then
return nil
end
for i = len - pad_len + 1, len do
if string.byte(str, i) ~= pad_len then
return nil
end
end
return string.sub(str, 1, len - pad_len)
end
local function zero_unpad(str)
local len = #str
while len > 0 and string.byte(str, len) == 0 do
len = len - 1
end
return string.sub(str, 1, len)
end
local function unpad(str)
local unpadded = pkcs7_unpad(str)
if unpadded then
return unpadded
else
return zero_unpad(str)
end
end
local function encrypt(state, roundKeys)
addRoundKey(state, roundKeys[0])
for round = 1, 13 do
subBytes(state, s)
shiftRows(state)
mixColumns(state)
addRoundKey(state, roundKeys[round])
end
subBytes(state, s)
shiftRows(state)
addRoundKey(state, roundKeys[14])
end
local function decrypt(state, roundKeys)
addRoundKey(state, roundKeys[14])
inv_shiftRows(state)
subBytes(state, si)
for round = 13, 1, -1 do
addRoundKey(state, roundKeys[round])
inv_mixColumns(state)
inv_shiftRows(state)
subBytes(state, si)
end
addRoundKey(state, roundKeys[0])
end
local function ECB_encrypt(key, originaltext)
local text = {}
local roundKeys = schedule256(key)
local i = 0
while true do
i, state = chunks(originaltext, i)
if not state then break end
encrypt(state, roundKeys)
text[i] = string.char(unpack(state))
end
return table.concat(text)
end
local function ECB_decrypt(key, ciphertext)
local text = {}
local roundKeys = schedule256(key)
local i = 0
while true do
i, state = chunks(ciphertext, i)
if not state then break end
decrypt(state, roundKeys)
text[i] = string.char(unpack(state))
end
return unpad(table.concat(text))
end
local function CBC_encrypt(key, iv, originaltext)
local roundKeys = schedule256(key)
local text = {}
local prev_block = {unpack(iv)}
local i = 0
while true do
i, block = chunks(originaltext, i)
if not block then break end
local xored = xor_blocks_8(block, prev_block)
encrypt(xored, roundKeys)
text[i] = string.char(unpack(xored))
prev_block = xored
end
return table.concat(text)
end
local function CBC_decrypt(key, iv, ciphertext)
local roundKeys = schedule256(key)
local text = {}
local prev_block = {unpack(iv)}
local i = 0
while true do
i, block = chunks(ciphertext, i)
if not block then break end
local decrypted = {unpack(block)}
decrypt(decrypted, roundKeys)
local plain_block = xor_blocks_8(decrypted, prev_block)
text[i] = string.char(unpack(plain_block))
prev_block = block
end
local result = table.concat(text)
return unpad(result)
end
return {
ECB = {
encrypt = ECB_encrypt;
decrypt = ECB_decrypt;
};
CBC = {
encrypt = CBC_encrypt;
decrypt = CBC_decrypt;
};
}
+203
View File
@@ -0,0 +1,203 @@
--[[
base64 -- v1.5.3 public domain Lua base64 encoder/decoder
no warranty implied; use at your own risk
Needs bit32.extract function. If not present it's implemented using BitOp
or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
implementation inspired by Rici Lake's post:
http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
author: Ilya Kolbin (iskolbin@gmail.com)
url: github.com/iskolbin/lbase64
COMPATIBILITY
Lua 5.1+, LuaJIT
LICENSE
See end of file for license information.
--]]
local base64 = {}
local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
if not extract then
if _G.bit then -- LuaJIT
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
extract = function( v, from, width )
return band( shr( v, from ), shl( 1, width ) - 1 )
end
elseif _G._VERSION == "Lua 5.1" then
extract = function( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
else -- Lua 5.3+
extract = load[[return function( v, from, width )
return ( v >> from ) & ((1 << width) - 1)
end]]()
end
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( str, encoder, usecaching )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #str
local lastn = n % 3
local cache = {}
for i = 1, n-lastn, 3 do
local a, b, c = str:byte( i, i+2 )
local v = a*0x10000 + b*0x100 + c
local s
if usecaching then
s = cache[v]
if not s then
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
cache[v] = s
end
else
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
end
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = str:byte( n-1, n )
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = str:byte( n )*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder, usecaching, schar1pos, schar2pos )
decoder = decoder or DEFAULT_DECODER
schar1pos = schar1pos or 62
schar2pos = schar2pos or 63
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == schar1pos then s62 = charcode
elseif b64code == schar2pos then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local cache = usecaching and {}
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
if usecaching then
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
s = cache[v0]
if not s then
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
cache[v0] = s
end
else
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
end
t[k] = s
k = k + 1
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = char( extract(v,16,8), extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = char( extract(v,16,8))
end
return concat( t )
end
return base64
--[[
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2018 Ilya Kolbin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
--]]
+157
View File
@@ -0,0 +1,157 @@
-- Clean up media name
local function clean_name(name)
return name:gsub("^%[.-%]", " ")
:gsub("^%(.-%)", " ")
:gsub("[_%.%[%]]", " ")
:gsub("第%s*%d+%s*季", "")
:gsub("第%s*%d+%s*部", "")
:gsub("第[一二三四五六七八九十]+季", "")
:gsub("第[一二三四五六七八九十]+部", "")
:gsub("^%s*(.-)%s*$", "%1")
:gsub("[!@#%.%?%+%-%%&*_=,/~`]+$", "")
end
-- Formatters for media titles
local formatters = {
{
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[_%.%s]%d%d[_%.%s]%d%d%s*[_%.%s]?(.-)%s*[_%.%s]%d+[pPkKxXbBfF]",
format = function(name, year, subtitle)
local title = clean_name(name)
if subtitle then
title = title .. ": " .. subtitle:gsub("%.", " "):gsub("^%s*(.-)%s*$", "%1")
end
return title .. " (" .. year .. ")"
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+%.?%d*)",
format = function(name, year, season, episode)
return clean_name(name) .. " (" .. year .. ") S" .. season .. "E" .. episode
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[eEpP]+[_%-%.%s]?(%d+%.?%d*)",
format = function(name, year, episode)
return clean_name(name) .. " (" .. year .. ") E" .. episode
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+[%.v]?%d*)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, season, episode, year)
return clean_name(name) .. " (" .. year .. ") S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+%.?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d+)[nrdsth]+[_%.%s]%s*[sS]eason[_%.%s]%s*%[(%d+[%.v]?%d*)%]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, episode, year)
return clean_name(name) .. " (" .. year .. ") E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%d+%.?%d*)",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
},
{
regex = "^(.-)%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%(%a+%)%]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[%-#]%s*(%d+%.?%d*)%s*",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
},
{
regex = "^(.-)%s*[%[%(]([OVADSPs]+)[%]%)]",
format = function(name, sp)
return clean_name(name) .. " [" .. sp .. "]"
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*(%d?%d)x(%d%d?%d?%d?)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode
end
},
{
regex = "^%((%d%d%d%d)%.?%d?%d?%.?%d?%d?%)%s*(.-)%s*[%(%[]",
format = function(year, name)
return clean_name(name) .. " (" .. year .. ")"
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, year)
return clean_name(name) .. " (" .. year .. ")"
end
},
{
regex = "^%[.-%]%s*%[?(.-)%]?%s*[%(%[]",
format = function(name)
return clean_name(name)
end
},
}
-- Format filename based on regex patterns
function format_filename(title)
for _, formatter in ipairs(formatters) do
local matches = {title:match(formatter.regex)}
if #matches > 0 then
title = formatter.format(unpack(matches))
return title
end
end
end
+192
View File
@@ -0,0 +1,192 @@
--[[
sha256 -- public domain Lua SHA-256 implementation
no warranty implied; use at your own risk
author: dyphire
COMPATIBILITY
Lua 5.1+, LuaJIT
LICENSE: MIT License
--]]
local unpack = unpack or table.unpack
local function band(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if aa == 1 and bb == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bor(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if aa == 1 or bb == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bxor(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if (aa + bb) == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bnot(a)
return 0xFFFFFFFF - a
end
local function lshift(a,n)
return (a * 2^n) % 2^32
end
local function rshift(a,n)
return math.floor(a / 2^n) % 2^32
end
local function bit_ror(x, n)
return bor(rshift(x, n), lshift(x, 32 - n))
end
local function sha256(message)
local k = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
}
local function preprocess(msg)
local len = #msg
local bitLen = len * 8
msg = msg .. "\128"
local zeroPad = 64 - ((len + 9) % 64)
if zeroPad ~= 64 then
msg = msg .. string.rep("\0", zeroPad)
end
msg = msg .. string.char(
rshift(bitLen, 56) % 256,
rshift(bitLen, 48) % 256,
rshift(bitLen, 40) % 256,
rshift(bitLen, 32) % 256,
rshift(bitLen, 24) % 256,
rshift(bitLen, 16) % 256,
rshift(bitLen, 8) % 256,
bitLen % 256
)
return msg
end
local function chunkify(msg)
local chunks = {}
for i = 1, #msg, 64 do
table.insert(chunks, msg:sub(i, i + 63))
end
return chunks
end
local function processChunk(chunk, hash)
local w = {}
for i = 1, 64 do
if i <= 16 then
w[i] = lshift(string.byte(chunk, (i - 1) * 4 + 1), 24) +
lshift(string.byte(chunk, (i - 1) * 4 + 2), 16) +
lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) +
string.byte(chunk, (i - 1) * 4 + 4)
else
local s0 = bxor(bxor(bit_ror(w[i - 15], 7), bit_ror(w[i - 15], 18)), rshift(w[i - 15], 3))
local s1 = bxor(bxor(bit_ror(w[i - 2], 17), bit_ror(w[i - 2], 19)), rshift(w[i - 2], 10))
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 2^32
end
end
local a, b, c, d, e, f, g, h = unpack(hash)
for i = 1, 64 do
local s1 = bxor(bxor(bit_ror(e, 6), bit_ror(e, 11)), bit_ror(e, 25))
local ch = bxor(band(e, f), band(bnot(e), g))
local temp1 = (h + s1 + ch + k[i] + w[i]) % 2^32
local s0 = bxor(bxor(bit_ror(a, 2), bit_ror(a, 13)), bit_ror(a, 22))
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
local temp2 = (s0 + maj) % 2^32
h = g
g = f
f = e
e = (d + temp1) % 2^32
d = c
c = b
b = a
a = (temp1 + temp2) % 2^32
end
return
(hash[1] + a) % 2^32,
(hash[2] + b) % 2^32,
(hash[3] + c) % 2^32,
(hash[4] + d) % 2^32,
(hash[5] + e) % 2^32,
(hash[6] + f) % 2^32,
(hash[7] + g) % 2^32,
(hash[8] + h) % 2^32
end
message = preprocess(message)
local chunks = chunkify(message)
local hash = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
}
for _, chunk in ipairs(chunks) do
hash = {processChunk(chunk, hash)}
end
local result = ""
for _, h in ipairs(hash) do
result = result .. string.format("%08x", h)
end
return result
end
return sha256
+164
View File
@@ -0,0 +1,164 @@
-- taken from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
-- modified from https://bitop.luajit.org/download.html (LuaBitOp-1.0.2 / md5test.lua)
-- and https://github.com/kikito/md5.lua/blob/master/md5.lua
-- SPDX-License-Identifier:MIT
local byte, char, sub, rep = string.byte, string.char, string.sub, string.rep
local tobit, tohex, bnot, bor, band, bxor, lshift, rshift, rol, bswap
if _G.bit then --LuaJIT
tobit, tohex = _G.bit.tobit or _G.bit.cast, _G.bit.tohex
bnot, bor, band, bxor, lshift, rshift = _G.bit.bnot, _G.bit.bor, _G.bit.band, _G.bit.bxor, _G.bit.lshift, _G.bit.rshift
rol, bswap = _G.bit.rol, _G.bit.bswap
elseif _G.bit32 then --Lua 5.2
local bit32_bnot = _G.bit32.bnot
tobit = function(a) return a <= 0x7fffffff and a or -(_G.bit32.bnot(a) + 1) end
bnot = function(a) return tobit(bit32_bnot(tobit(a))) end
bor, band, bxor, lshift, rshift, rol = _G.bit32.bor, _G.bit32.band, _G.bit32.bxor, _G.bit32.lshift, _G.bit32.rshift, _G.bit32.lrotate
else
return nil
end
if not tohex then
tohex = function(a) return string.sub(string.format('%08x', a), -8) end
end
if not bswap then
bswap = function(a)
return bor(rshift(a, 24), band(rshift(a, 8), 0xff00), lshift(band(a, 0xff00), 8), lshift(a, 24))
end
end
local function tr_f(a, b, c, d, x, s) return rol(bxor(d, band(b, bxor(c, d))) + a + x, s) + b end
local function tr_g(a, b, c, d, x, s) return rol(bxor(c, band(d, bxor(b, c))) + a + x, s) + b end
local function tr_h(a, b, c, d, x, s) return rol(bxor(b, c, d) + a + x, s) + b end
local function tr_i(a, b, c, d, x, s) return rol(bxor(c, bor(b, bnot(d))) + a + x, s) + b end
local function transform(x, a1, b1, c1, d1)
local a, b, c, d = a1, b1, c1, d1
a = tr_f(a, b, c, d, x[1] + 0xd76aa478, 7)
d = tr_f(d, a, b, c, x[2] + 0xe8c7b756, 12)
c = tr_f(c, d, a, b, x[3] + 0x242070db, 17)
b = tr_f(b, c, d, a, x[4] + 0xc1bdceee, 22)
a = tr_f(a, b, c, d, x[5] + 0xf57c0faf, 7)
d = tr_f(d, a, b, c, x[6] + 0x4787c62a, 12)
c = tr_f(c, d, a, b, x[7] + 0xa8304613, 17)
b = tr_f(b, c, d, a, x[8] + 0xfd469501, 22)
a = tr_f(a, b, c, d, x[9] + 0x698098d8, 7)
d = tr_f(d, a, b, c, x[10] + 0x8b44f7af, 12)
c = tr_f(c, d, a, b, x[11] + 0xffff5bb1, 17)
b = tr_f(b, c, d, a, x[12] + 0x895cd7be, 22)
a = tr_f(a, b, c, d, x[13] + 0x6b901122, 7)
d = tr_f(d, a, b, c, x[14] + 0xfd987193, 12)
c = tr_f(c, d, a, b, x[15] + 0xa679438e, 17)
b = tr_f(b, c, d, a, x[16] + 0x49b40821, 22)
a = tr_g(a, b, c, d, x[2] + 0xf61e2562, 5)
d = tr_g(d, a, b, c, x[7] + 0xc040b340, 9)
c = tr_g(c, d, a, b, x[12] + 0x265e5a51, 14)
b = tr_g(b, c, d, a, x[1] + 0xe9b6c7aa, 20)
a = tr_g(a, b, c, d, x[6] + 0xd62f105d, 5)
d = tr_g(d, a, b, c, x[11] + 0x02441453, 9)
c = tr_g(c, d, a, b, x[16] + 0xd8a1e681, 14)
b = tr_g(b, c, d, a, x[5] + 0xe7d3fbc8, 20)
a = tr_g(a, b, c, d, x[10] + 0x21e1cde6, 5)
d = tr_g(d, a, b, c, x[15] + 0xc33707d6, 9)
c = tr_g(c, d, a, b, x[4] + 0xf4d50d87, 14)
b = tr_g(b, c, d, a, x[9] + 0x455a14ed, 20)
a = tr_g(a, b, c, d, x[14] + 0xa9e3e905, 5)
d = tr_g(d, a, b, c, x[3] + 0xfcefa3f8, 9)
c = tr_g(c, d, a, b, x[8] + 0x676f02d9, 14)
b = tr_g(b, c, d, a, x[13] + 0x8d2a4c8a, 20)
a = tr_h(a, b, c, d, x[6] + 0xfffa3942, 4)
d = tr_h(d, a, b, c, x[9] + 0x8771f681, 11)
c = tr_h(c, d, a, b, x[12] + 0x6d9d6122, 16)
b = tr_h(b, c, d, a, x[15] + 0xfde5380c, 23)
a = tr_h(a, b, c, d, x[2] + 0xa4beea44, 4)
d = tr_h(d, a, b, c, x[5] + 0x4bdecfa9, 11)
c = tr_h(c, d, a, b, x[8] + 0xf6bb4b60, 16)
b = tr_h(b, c, d, a, x[11] + 0xbebfbc70, 23)
a = tr_h(a, b, c, d, x[14] + 0x289b7ec6, 4)
d = tr_h(d, a, b, c, x[1] + 0xeaa127fa, 11)
c = tr_h(c, d, a, b, x[4] + 0xd4ef3085, 16)
b = tr_h(b, c, d, a, x[7] + 0x04881d05, 23)
a = tr_h(a, b, c, d, x[10] + 0xd9d4d039, 4)
d = tr_h(d, a, b, c, x[13] + 0xe6db99e5, 11)
c = tr_h(c, d, a, b, x[16] + 0x1fa27cf8, 16)
b = tr_h(b, c, d, a, x[3] + 0xc4ac5665, 23)
a = tr_i(a, b, c, d, x[1] + 0xf4292244, 6)
d = tr_i(d, a, b, c, x[8] + 0x432aff97, 10)
c = tr_i(c, d, a, b, x[15] + 0xab9423a7, 15)
b = tr_i(b, c, d, a, x[6] + 0xfc93a039, 21)
a = tr_i(a, b, c, d, x[13] + 0x655b59c3, 6)
d = tr_i(d, a, b, c, x[4] + 0x8f0ccc92, 10)
c = tr_i(c, d, a, b, x[11] + 0xffeff47d, 15)
b = tr_i(b, c, d, a, x[2] + 0x85845dd1, 21)
a = tr_i(a, b, c, d, x[9] + 0x6fa87e4f, 6)
d = tr_i(d, a, b, c, x[16] + 0xfe2ce6e0, 10)
c = tr_i(c, d, a, b, x[7] + 0xa3014314, 15)
b = tr_i(b, c, d, a, x[14] + 0x4e0811a1, 21)
a = tr_i(a, b, c, d, x[5] + 0xf7537e82, 6)
d = tr_i(d, a, b, c, x[12] + 0xbd3af235, 10)
c = tr_i(c, d, a, b, x[3] + 0x2ad7d2bb, 15)
b = tr_i(b, c, d, a, x[10] + 0xeb86d391, 21)
return tobit(a + a1), tobit(b + b1), tobit(c + c1), tobit(d + d1)
end
local function md5_update(self, s)
local m, len = s, #s
if len % 4 ~= 0 then
m = m .. '\128' .. rep('\0', 63 - band(len + 8, 63)) ..
char(band(lshift(len, 3), 255), band(rshift(len, 5), 255), band(rshift(len, 13), 255),
band(rshift(len, 21), 255)) .. '\0\0\0\0'
end
local a, b, c, d = self.a, self.b, self.c, self.d
local x, k = self.x, self.k
for i = 1, #m, 4 do
local m0, m1, m2, m3 = byte(m, i, i + 3)
x[k] = bor(m0, lshift(m1, 8), lshift(m2, 16), lshift(m3, 24))
if k == 16 then
a, b, c, d = transform(x, a, b, c, d)
k = 1
else
k = k + 1
end
end
self.a, self.b, self.c, self.d, self.k = a, b, c, d, k
self.len = self.len + len
return self
end
local function md5_finish(self)
local len = self.len
if len % 4 == 0 then
local s = '\128' .. rep('\0', 63 - band(len + 8, 63)) ..
char(band(lshift(len, 3), 255), band(rshift(len, 5), 255), band(rshift(len, 13), 255),
band(rshift(len, 21), 255)) .. '\0\0\0\0'
md5_update(self, s)
end
return tohex(bswap(self.a)) .. tohex(bswap(self.b)) .. tohex(bswap(self.c)) .. tohex(bswap(self.d))
end
local md5 = {}
function md5.new()
return {
a = 0x67452301,
b = 0xefcdab89,
c = 0x98badcfe,
d = 0x10325476,
x = {},
k = 1,
len = 0,
update = md5_update,
finish = md5_finish,
}
end
function md5.sum(s)
return md5.new():update(s):finish()
end
return md5
+806
View File
@@ -0,0 +1,806 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
input_loaded, input = pcall(require, "mp.input")
uosc_available = false
-- 打开番剧数据匹配菜单
function get_animes(query)
local encoded_query = url_encode(query)
local url = options.api_server .. "/api/v2/search/anime"
local params = "keyword=" .. encoded_query
local full_url = url .. "?" .. params
local items = {}
local message = "加载数据中..."
local menu_type = "menu_anime"
local menu_title = "在此处输入番剧名称"
local footnote = "使用enter或ctrl+enter进行搜索"
local menu_cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
else
show_message(message, 30)
end
msg.verbose("尝试获取番剧数据:" .. full_url)
local args = make_danmaku_request_args("GET", full_url)
if args == nil then
return
end
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args })
if not res.status or res.status ~= 0 then
local message = "获取数据失败"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
else
show_message(message, 3)
end
msg.error("HTTP 请求失败:" .. res.stderr)
end
local response = utils.parse_json(res.stdout)
if not response or not response.animes then
local message = "无结果"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
else
show_message(message, 3)
end
msg.info("无结果")
return
end
for _, anime in ipairs(response.animes) do
table.insert(items, {
title = anime.animeTitle,
hint = anime.typeDescription,
value = {
"script-message-to",
mp.get_script_name(),
"search-episodes-event",
anime.animeTitle, anime.bangumiId,
},
})
end
if uosc_available then
update_menu_uosc(menu_type, menu_title, items, footnote, menu_cmd, query)
elseif input_loaded then
show_message("", 0)
mp.add_timeout(0.1, function()
open_menu_select(items)
end)
end
end
function get_episodes(animeTitle, bangumiId)
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
local items = {}
local message = "加载数据中..."
local menu_type = "menu_episodes"
local menu_title = "剧集信息"
local footnote = "使用 / 打开筛选"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 30)
end
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args })
if not res.status or res.status ~= 0 then
local message = "获取数据失败"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.error("HTTP 请求失败:" .. res.stderr)
end
local response = utils.parse_json(res.stdout)
if not response or not response.bangumi or not response.bangumi.episodes then
local message = "无结果"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.info("无结果")
return
end
for _, episode in ipairs(response.bangumi.episodes) do
table.insert(items, {
title = episode.episodeTitle,
hint = episode.episodeNumber,
value = { "script-message-to", mp.get_script_name(), "load-danmaku",
animeTitle, episode.episodeTitle, episode.episodeId },
keep_open = false,
selectable = true,
})
end
if uosc_available then
update_menu_uosc(menu_type, menu_title, items, footnote)
elseif input_loaded then
mp.add_timeout(0.1, function()
open_menu_select(items)
end)
end
end
function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_cmd, query)
local items = {}
if type(menu_item) == "string" then
table.insert(items, {
title = menu_item,
value = "",
italic = true,
keep_open = true,
selectable = false,
align = "center",
})
else
items = menu_item
end
local menu_props = {
type = menu_type,
title = menu_title,
search_style = menu_cmd and "palette" or "on_demand",
search_debounce = menu_cmd and "submit" or 0,
on_search = menu_cmd,
footnote = menu_footnote,
search_suggestion = query,
items = items,
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_menu_select(menu_items, is_time)
local item_titles, item_values = {}, {}
for i, v in ipairs(menu_items) do
item_titles[i] = is_time and "[" .. v.hint .. "] " .. v.title or
(v.hint and v.title .. " (" .. v.hint .. ")" or v.title)
item_values[i] = v.value
end
mp.commandv('script-message-to', 'console', 'disable')
input.select({
prompt = '筛选:',
items = item_titles,
submit = function(id)
mp.commandv(unpack(item_values[id]))
end,
})
end
-- 打开弹幕输入搜索菜单
function open_input_menu_get()
mp.commandv('script-message-to', 'console', 'disable')
local title = parse_title()
input.get({
prompt = '番剧名称:',
default_text = title,
cursor_position = title and #title + 1,
submit = function(text)
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "search-anime-event", text)
end
})
end
function open_input_menu_uosc()
local items = {}
if DANMAKU.anime and DANMAKU.episode then
local episode = DANMAKU.episode:gsub("%s.-$","")
episode = episode:match("^(第.*[话回集]+)%s*") or episode
items[#items + 1] = {
title = string.format("已关联弹幕:%s-%s", DANMAKU.anime, episode),
bold = true,
italic = true,
keep_open = true,
selectable = false,
}
end
items[#items + 1] = {
hint = " 追加|ds或|dy或|dm可搜索电视剧|电影|国漫",
keep_open = true,
selectable = false,
}
local menu_props = {
type = "menu_danmaku",
title = "在此处输入番剧名称",
search_style = "palette",
search_debounce = "submit",
search_suggestion = parse_title(),
on_search = { "script-message-to", mp.get_script_name(), "search-anime-event" },
footnote = "使用enter或ctrl+enter进行搜索",
items = items
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_input_menu()
if uosc_available then
open_input_menu_uosc()
elseif input_loaded then
open_input_menu_get()
end
end
-- 打开弹幕源添加管理菜单
function open_add_menu_get()
mp.commandv('script-message-to', 'console', 'disable')
input.get({
prompt = 'Input url:',
submit = function(text)
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "add-source-event", text)
end
})
end
function open_add_menu_uosc()
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname then
local item = {title = url, value = url, keep_open = true,}
if source.from == "api_server" then
if source.blocked then
item.hint = "来源:弹幕服务器(已屏蔽)"
item.actions = {{icon = "check", name = "unblock"},}
else
item.hint = "来源:弹幕服务器(未屏蔽)"
item.actions = {{icon = "not_interested", name = "block"},}
end
else
item.hint = "来源:用户添加"
item.actions = {{icon = "delete", name = "delete"},}
end
table.insert(sources, item)
end
end
local menu_props = {
type = "menu_source",
title = "在此输入源地址url",
search_style = "palette",
search_debounce = "submit",
on_search = { "script-message-to", mp.get_script_name(), "add-source-event" },
footnote = "使用enter或ctrl+enter进行添加",
items = sources,
item_actions_place = "outside",
callback = {mp.get_script_name(), 'setup-danmaku-source'},
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_add_menu()
if uosc_available then
open_add_menu_uosc()
elseif input_loaded then
open_add_menu_get()
end
end
-- 打开弹幕内容菜单
function open_content_menu(pos)
local items = {}
local time_pos = pos or mp.get_property_native("time-pos")
local duration = mp.get_property_number("duration", 0)
if COMMENTS ~= nil then
for _, event in ipairs(COMMENTS) do
local text = event.clean_text:gsub("^m%s[mbl%s%-%d%.]+$", ""):gsub("^%s*(.-)%s*$", "%1")
local delay = get_delay_for_time(DELAYS, event.start_time)
local start_time = event.start_time + delay
local end_time = event.end_time + delay
if text and text ~= "" and start_time >= 0 and start_time <= duration then
table.insert(items, {
title = abbr_str(text, 60),
hint = seconds_to_time(start_time),
value = { "seek", start_time, "absolute" },
active = time_pos >= start_time and time_pos <= end_time,
})
end
end
end
local menu_props = {
type = "menu_content",
title = "弹幕内容",
footnote = "使用 / 打开搜索",
items = items
}
local json_props = utils.format_json(menu_props)
if uosc_available then
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
elseif input_loaded then
open_menu_select(items, true)
end
end
local menu_items_config = {
bold = { title = "粗体", hint = options.bold, original = options.bold,
footnote = "true / false", },
fontsize = { title = "大小", hint = options.fontsize, original = options.fontsize,
scope = { min = 0, max = math.huge }, footnote = "请输入整数(>=0)", },
outline = { title = "描边", hint = options.outline, original = options.outline,
scope = { min = 0.0, max = 4.0 }, footnote = "输入范围:(0.0-4.0)" },
shadow = { title = "阴影", hint = options.shadow, original = options.shadow,
scope = { min = 0, max = math.huge }, footnote = "请输入整数(>=0)", },
scrolltime = { title = "速度", hint = options.scrolltime, original = options.scrolltime,
scope = { min = 1, max = math.huge }, footnote = "请输入整数(>=1)", },
opacity = { title = "透明度", hint = options.opacity, original = options.opacity,
scope = { min = 0, max = 1 }, footnote = "输入范围0完全透明到1不透明", },
displayarea = { title = "弹幕显示范围", hint = options.displayarea, original = options.displayarea,
scope = { min = 0.0, max = 1.0 }, footnote = "显示范围(0.0-1.0)", },
}
-- 创建一个包含键顺序的表,这是样式菜单的排布顺序
local ordered_keys = {"bold", "fontsize", "outline", "shadow", "scrolltime", "opacity", "displayarea"}
-- 设置弹幕样式菜单
function add_danmaku_setup(actived, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
end
local items = {}
for _, key in ipairs(ordered_keys) do
local config = menu_items_config[key]
local item_config = {
title = config.title,
hint = "目前:" .. tostring(config.hint),
active = key == actived,
keep_open = true,
selectable = true,
}
if config.hint ~= config.original then
local original_str = tostring(config.original)
item_config.actions = {{icon = "refresh", name = key, label = "恢复默认配置 < " .. original_str .. " >"}}
end
table.insert(items, item_config)
end
local menu_props = {
type = "menu_style",
title = "弹幕样式",
search_style = "disabled",
footnote = "样式更改仅在本次播放生效",
item_actions_place = "outside",
items = items,
callback = { mp.get_script_name(), 'setup-danmaku-style'},
}
local actions = "open-menu"
if status ~= nil then
-- msg.info(status)
if status == "updata" then
-- "updata" 模式会保留输入框文字
menu_props.title = " " .. menu_items_config[actived]["footnote"]
actions = "update-menu"
elseif status == "refresh" then
-- "refresh" 模式会清除输入框文字
menu_props.title = " " .. menu_items_config[actived]["footnote"]
elseif status == "error" then
menu_props.title = "输入非数字字符或范围出错"
-- 创建一个定时器在1秒后触发回调函数删除搜索栏错误信息
mp.add_timeout(1.0, function() add_danmaku_setup(actived, "updata") end)
end
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
menu_props.footnote = menu_items_config[actived]["footnote"] or ""
menu_props.on_search = { "script-message-to", mp.get_script_name(), "setup-danmaku-style", actived }
end
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", actions, json_props)
end
-- 设置弹幕源延迟菜单
function danmaku_delay_setup(source_url)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
end
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname and not source.blocked then
local delay = 0
if source.delay_segments then
for _, seg in ipairs(source.delay_segments) do
if seg.start == 0 then
delay = seg.delay or 0
break
end
end
end
local item = {title = url, value = url, keep_open = true,}
item.hint = "当前弹幕源延迟:" .. string.format("%.1f", delay + 1e-10) .. ""
item.active = url == source_url
table.insert(sources, item)
end
end
local menu_props = {
type = "menu_delay",
title = "弹幕源延迟设置",
search_style = "disabled",
items = sources,
callback = {mp.get_script_name(), 'setup-source-delay'},
}
if source_url ~= nil then
menu_props.title = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
menu_props.on_search = { "script-message-to", mp.get_script_name(), "setup-source-delay", source_url }
end
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
-- 总集合弹幕菜单
function open_add_total_menu_uosc()
local items = {}
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕源延迟设置", action = "open_source_delay_menu" },
{ title = "弹幕样式", action = "open_setup_danmaku_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
if DANMAKU.anime and DANMAKU.episode then
local episode = DANMAKU.episode:gsub("%s.-$","")
episode = episode:match("^(第.*[话回集]+)%s*") or episode
items[#items + 1] = {
title = string.format("已关联弹幕:%s-%s", DANMAKU.anime, episode),
bold = true,
italic = true,
keep_open = true,
selectable = false,
}
end
for _, config in ipairs(total_menu_items_config) do
table.insert(items, {
title = config.title,
value = { "script-message-to", mp.get_script_name(), config.action },
keep_open = false,
selectable = true,
})
end
local menu_props = {
type = "menu_total",
title = "弹幕设置",
search_style = "disabled",
items = items,
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_add_total_menu_select()
local item_titles, item_values = {}, {}
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
for i, config in ipairs(total_menu_items_config) do
item_titles[i] = config.title
item_values[i] = { "script-message-to", mp.get_script_name(), config.action }
end
mp.commandv('script-message-to', 'console', 'disable')
input.select({
prompt = '选择:',
items = item_titles,
submit = function(id)
mp.commandv(unpack(item_values[id]))
end,
})
end
function open_add_total_menu()
if uosc_available then
open_add_total_menu_uosc()
elseif input_loaded then
open_add_total_menu_select()
end
end
-- 添加 uosc 菜单栏按钮
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku",
utils.format_json({
icon = "search",
tooltip = "弹幕搜索",
command = "script-message open_search_danmaku_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_source",
utils.format_json({
icon = "add_box",
tooltip = "从源添加弹幕",
command = "script-message open_add_source_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_styles",
utils.format_json({
icon = "palette",
tooltip = "弹幕样式",
command = "script-message open_setup_danmaku_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_delay",
utils.format_json({
icon = "more_time",
tooltip = "弹幕源延迟设置",
command = "script-message open_source_delay_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_menu",
utils.format_json({
icon = "grid_view",
tooltip = "弹幕设置",
command = "script-message open_add_total_menu",
})
)
mp.register_script_message('uosc-version', function()
uosc_available = true
end)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "off")
mp.register_script_message("set", function(prop, value)
if prop ~= "show_danmaku" then
return
end
if value == "on" then
ENABLED = true
set_danmaku_visibility(true)
if COMMENTS == nil then
local path = mp.get_property("path")
init(path)
else
show_loaded()
show_danmaku_func()
end
else
show_message("关闭弹幕", 2)
ENABLED = false
set_danmaku_visibility(false)
hide_danmaku_func()
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", value)
end)
-- 注册函数给 uosc 按钮使用
mp.register_script_message("search-anime-event", function(query)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_danmaku")
end
local name, class = query:match("^(.-)%s*|%s*(.-)%s*$")
if name and class then
query_extra(name, class)
else
get_animes(query)
end
end)
mp.register_script_message("search-episodes-event", function(animeTitle, bangumiId)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime")
end
get_episodes(animeTitle, bangumiId)
end)
-- Register script message to show the input menu
mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId)
ENABLED = true
DANMAKU.anime = animeTitle
DANMAKU.episode = episodeTitle
set_episode_id(episodeId, true)
end)
mp.register_script_message("add-source-event", function(query)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
end
ENABLED = true
add_danmaku_source(query, true)
end)
mp.register_script_message("open_setup_danmaku_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
end
add_danmaku_setup()
end)
mp.register_script_message("open_content_danmaku_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
end
open_content_menu()
end)
mp.register_script_message("setup-danmaku-style", function(query, text)
local event = utils.parse_json(query)
if event ~= nil then
-- item点击 或 图标点击
if event.type == "activate" then
if not event.action then
if ordered_keys[event.index] == "bold" then
options.bold = not options.bold
menu_items_config.bold.hint = options.bold and "true" or "false"
end
-- "updata" 模式会保留输入框文字
add_danmaku_setup(ordered_keys[event.index], "updata")
return
else
-- msg.info("event.action" .. event.action)
options[event.action] = menu_items_config[event.action]["original"]
menu_items_config[event.action]["hint"] = options[event.action]
add_danmaku_setup(event.action, "updata")
if event.action == "fontsize" or event.action == "scrolltime" then
load_danmaku(true)
end
end
end
else
-- 数值输入
if text == nil or text == "" then
return
end
local newText, _ = text:gsub("%s", "") -- 移除所有空白字符
if tonumber(newText) ~= nil and menu_items_config[query]["scope"] ~= nil then
local num = tonumber(newText)
local min_num = menu_items_config[query]["scope"]["min"]
local max_num = menu_items_config[query]["scope"]["max"]
if num and min_num <= num and num <= max_num then
if string.match(menu_items_config[query]["footnote"], "整数") then
-- 输入范围为整数时向下取整
num = tostring(math.floor(num))
end
options[query] = tostring(num)
menu_items_config[query]["hint"] = options[query]
-- "refresh" 模式会清除输入框文字
add_danmaku_setup(query, "refresh")
if query == "fontsize" or query == "scrolltime" then
load_danmaku(true, true)
end
return
end
end
add_danmaku_setup(query, "error")
end
end)
mp.register_script_message('setup-danmaku-source', function(json)
local event = utils.parse_json(json)
if event.type == 'activate' then
if event.action == "delete" then
local rm = DANMAKU.sources[event.value]["fname"]
if rm and file_exists(rm) and DANMAKU.sources[event.value]["from"] ~= "user_local" then
os.remove(rm)
end
DANMAKU.sources[event.value] = nil
remove_source_from_history(event.value)
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
load_danmaku(true)
end
if event.action == "block" then
DANMAKU.sources[event.value]["blocked"] = true
add_source_to_history(event.value, DANMAKU.sources[event.value])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
load_danmaku(true)
end
if event.action == "unblock" then
DANMAKU.sources[event.value]["blocked"] = false
add_source_to_history(event.value, DANMAKU.sources[event.value])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
load_danmaku(true)
end
end
end)
mp.register_script_message("setup-source-delay", function(query, text)
local event = utils.parse_json(query)
if event ~= nil then
-- item点击
if event.type == "activate" then
danmaku_delay_setup(event.value)
end
else
-- 数值输入
if text == nil or text == "" then
return
end
local newText, _ = text:gsub("%s", "") -- 移除所有空白字符
local num = tonumber(newText)
local delay_segments = shallow_copy(DANMAKU.sources[query]["delay_segments"] or {})
for i = #delay_segments, 1, -1 do
if delay_segments[i].start == 0 then
table.remove(delay_segments, i)
end
end
if num ~= nil then
table.insert(delay_segments, 1, { start = 0, delay = tonumber(num) })
DANMAKU.sources[query]["delay_segments"] = delay_segments
add_source_to_history(query, DANMAKU.sources[query])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
danmaku_delay_setup(query)
load_danmaku(true, true)
elseif newText:match("^%-?%d+m%d+s$") then
local minutes, seconds = string.match(newText, "^(%-?%d+)m(%d+)s$")
minutes = tonumber(minutes)
seconds = tonumber(seconds)
if minutes < 0 then seconds = -seconds end
table.insert(delay_segments, 1, { start = 0, delay = 60 * minutes + seconds })
DANMAKU.sources[query]["delay_segments"] = delay_segments
add_source_to_history(query, DANMAKU.sources[query])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
danmaku_delay_setup(query)
load_danmaku(true, true)
end
end
end)
+76
View File
@@ -0,0 +1,76 @@
local opt = require("mp.options")
-- 选项
options = {
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
api_server = "https://api.dandanplay.net",
-- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
-- 服务器可以自托管https://github.com/lyz05/danmaku
fallback_server = "https://fc.lyz05.cn",
-- 设置 tmdb 的 API Key用于获取非动画条目的中文信息(当搜索内容非中文时)
-- 可以在 https://www.themoviedb.org 注册后去个人账号设置界面获取
-- 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码
tmdb_api_key = "NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM=",
load_more_danmaku = false,
auto_load = false,
autoload_local_danmaku = false,
autoload_for_url = false,
save_danmaku = false,
user_agent = "mpv_danmaku/1.0",
proxy = "",
-- 使用 fps 视频滤镜,大幅提升弹幕平滑度。默认禁用
vf_fps = false,
-- 设置要使用的 fps 滤镜参数
fps = "60/1.001",
-- 指定合并重复弹幕的时间间隔的容差值,单位为秒。默认值: -1表示禁用
merge_tolerance = -1,
-- 指定弹幕关联历史记录文件的路径,支持绝对路径和相对路径
history_path = "~~/danmaku-history.json",
open_search_danmaku_menu_key = "Ctrl+d",
show_danmaku_keyboard_key = "j",
-- 中文简繁转换。0-不转换1-转换为简体2-转换为繁体
chConvert = 0,
--滚动弹幕的显示时间
scrolltime = 15,
--固定弹幕的显示时间
fixtime = 5,
--字体
fontname = "sans-serif",
--字体大小
fontsize = 50,
--字体阴影
shadow = 0,
--字体粗体
bold = true,
-- 透明度0完全透明到 1不透明
opacity = 0.7,
--全部弹幕的显示范围(0.0-1.0)
displayarea = 0.85,
--描边 0-4
outline = 1.0,
-- 限制屏幕中同时显示的最大弹幕数量0 表示不限制
max_screen_danmaku = 0,
--指定弹幕屏蔽词文件路径(black.txt),支持绝对路径和相对路径。文件内容以换行分隔
--支持 lua 的正则表达式写法
blacklist_path = "",
--指定脚本相关消息显示的消息的对齐方式
message_anlignment = 7,
--指定脚本相关消息显示的消息的x轴坐标
message_x = 30,
--指定脚本相关消息显示的消息的y轴坐标
message_y = 30,
-- 自定义标题解析中的额外替换规则,内容格式为 JSON 字符串,替换模式为 lua 的 string.gsub 函数
--! 注意:由于 mpv 的 lua 版本限制,自定义规则只支持形如 %n 的捕获组写法,即示例用法,不支持直接替换字符的写法
title_replace = [[
[{
"rules": [{ "^(.-)": "%1"},{ "^.*《(.-)》": "%1" }],
}]
]],
-- 指定哈希匹配中需忽略的共享盘(挂载盘)的路径/目录。支持绝对路径和相对路径,多个路径用逗号分隔
-- 示例:["X:", "Z:", "F:/Download/", "Download"]
excluded_path = [[
[]
]],
}
opt.read_options(options, mp.get_script_name(), function() end)
+655
View File
@@ -0,0 +1,655 @@
local msg = require 'mp.msg'
local utils = require 'mp.utils'
local s2t = require("dicts/s2t_chars")
local t2s = require("dicts/t2s_chars")
local function ass_escape(text)
return text:gsub("\\", "\\\\")
:gsub("{", "\\{")
:gsub("}", "\\}")
:gsub("\n", "\\N")
end
local function xml_unescape(str)
return str:gsub("&quot;", "\"")
:gsub("&apos;", "'")
:gsub("&gt;", ">")
:gsub("&lt;", "<")
:gsub("&amp;", "&")
end
local function decode_html_entities(text)
return text:gsub("&#x([%x]+);", function(hex)
local codepoint = tonumber(hex, 16)
return unicode_to_utf8(codepoint)
end):gsub("&#(%d+);", function(dec)
local codepoint = tonumber(dec, 10)
return unicode_to_utf8(codepoint)
end)
end
-- 加载黑名单模式
local function load_blacklist_patterns(filepath)
local patterns = {}
if not file_exists(filepath) then
return patterns
end
local file = io.open(filepath, "r")
if not file then
msg.error("无法打开黑名单文件: " .. filepath)
return patterns
end
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
end
end
file:close()
return patterns
end
local blacklist_file = mp.command_native({ "expand-path", options.blacklist_path })
local black_patterns = load_blacklist_patterns(blacklist_file)
-- 检查字符串是否在黑名单中
function is_blacklisted(str, patterns)
for _, pattern in ipairs(patterns) do
local ok, result = pcall(function()
return str:match(pattern)
end)
if ok and result then
return true, pattern
elseif not ok then
-- msg.debug("黑名单规则错误,跳过: " .. pattern .. ",错误信息:" .. result)
end
end
return false
end
-- 简繁转换
local function convert(text, dict)
return text:gsub("[%z\1-\127\194-\244][\128-\191]*", function(c)
return dict[c] or c
end)
end
local function ch_convert(str)
if options.chConvert == 1 then
return convert(str, t2s)
elseif options.chConvert == 2 then
return convert(str, s2t)
end
return str
end
local ch_convert_cache = {}
local ch_cache_keys = {}
local ch_cache_max = 5000
local function ch_convert_cached(text)
if type(text) ~= "string" or text == "" then return text end
local cached = ch_convert_cache[text]
if cached ~= nil then return cached end
local converted = ch_convert(text)
ch_convert_cache[text] = converted
ch_cache_keys[#ch_cache_keys+1] = text
if #ch_cache_keys > ch_cache_max then
local old_key = table.remove(ch_cache_keys, 1)
ch_convert_cache[old_key] = nil
end
return converted
end
-- 合并重复弹幕
local function merge_duplicate_danmaku(danmakus, threshold)
if not threshold or tonumber(threshold) < 0 then return danmakus end
local groups = {}
for _, d in ipairs(danmakus) do
local key = d.type .. "|" .. d.color .. "|" .. d.text
if not groups[key] then groups[key] = {} end
table.insert(groups[key], d)
end
local merged = {}
for _, group in pairs(groups) do
table.sort(group, function(a, b) return a.time < b.time end)
local i = 1
while i <= #group do
local base = group[i]
local times = { base.time }
local count = 1
local j = i + 1
while j <= #group and math.abs(group[j].time - base.time) <= threshold do
table.insert(times, group[j].time)
count = count + 1
j = j + 1
end
local same_time = true
for k = 2, #times do
if times[k] ~= times[1] then
same_time = false
break
end
end
local danmaku = {
time = base.time,
type = base.type,
size = base.size,
color = base.color,
text = base.text,
}
if count > 2 or not same_time then
danmaku.text = danmaku.text .. string.format("x%d", count)
end
table.insert(merged, danmaku)
i = j
end
end
table.sort(merged, function(a, b) return a.time < b.time end)
return merged
end
-- 限制每屏弹幕条数
local function limit_danmaku(danmakus, limit)
if not limit or limit <= 0 then
return danmakus
end
local window = {}
for _, d in ipairs(danmakus) do
for i = #window, 1, -1 do
if window[i].end_time <= d.start_time then
table.remove(window, i)
end
end
if #window < limit then
table.insert(window, d)
else
local max_idx = 1
for i = 2, #window do
if window[i].end_time > window[max_idx].end_time then
max_idx = i
end
end
if window[max_idx].end_time > d.end_time then
window[max_idx].drop = true
window[max_idx] = d
else
d.drop = true
end
end
end
local result = {}
for _, d in ipairs(danmakus) do
if not d.drop then
table.insert(result, d)
end
end
return result
end
-- 解析 XML 弹幕
local function parse_xml_danmaku(xml_string, delay_segments)
local danmakus = {}
for p_attr, text in xml_string:gmatch('<d p="([^"]+)">([^<]+)</d>') do
local params = {}
local i = 1
for val in p_attr:gmatch("([^,]+)") do
params[i] = tonumber(val)
i = i + 1
end
if params[1] and params[2] and params[3] and params[4] then
local base_time = params[1]
local delay = get_delay_for_time(delay_segments, base_time)
table.insert(danmakus, {
time = base_time + delay,
type = params[2] or 1,
size = params[3] or 25,
color = params[4] or 0xFFFFFF,
text = xml_unescape(text)
})
end
end
table.sort(danmakus, function(a, b) return a.time < b.time end)
return danmakus
end
-- 解析 JSON 弹幕
local function parse_json_danmaku(json_string, delay_segments)
local danmakus = {}
if json_string:sub(1, 3) == "\239\187\191" then
json_string = json_string:sub(4)
end
local json = utils.parse_json(json_string)
if not json or type(json) ~= "table" then
msg.info("JSON 解析失败")
return danmakus
end
for _, entry in ipairs(json) do
local c = entry.c
local text = entry.m or ""
if type(c) == "string" then
local params = {}
local i = 1
for val in c:gmatch("([^,]+)") do
params[i] = tonumber(val)
i = i + 1
end
if params[1] and params[2] and params[3] and params[4] then
local base_time = params[1]
local delay = get_delay_for_time(delay_segments, base_time)
table.insert(danmakus, {
time = base_time + delay,
color = params[2] or 0xFFFFFF,
type = params[3] or 1,
size = params[4] or 25,
text = text
})
end
end
end
table.sort(danmakus, function(a, b) return a.time < b.time end)
return danmakus
end
-- 解析弹幕文件
function parse_danmaku_files(danmaku_input, delays)
local DANMAKU_PATHs = {}
if type(danmaku_input) == "string" then
DANMAKU_PATHs = { danmaku_input }
else
for i, input in ipairs(danmaku_input) do
DANMAKU_PATHs[#DANMAKU_PATHs + 1] = input
end
end
local all_danmaku = {}
for i, DANMAKU_PATH in ipairs(DANMAKU_PATHs) do
if file_exists(DANMAKU_PATH) then
local content = read_file(DANMAKU_PATH)
if content then
local parsed = {}
local delay_segments = delays and delays[i] or {}
if DANMAKU_PATH:match("%.xml$") then
parsed = parse_xml_danmaku(content, delay_segments)
elseif DANMAKU_PATH:match("%.json$") then
parsed = parse_json_danmaku(content, delay_segments)
end
for _, d in ipairs(parsed) do
local matched, pattern = is_blacklisted(d.text, black_patterns)
if not matched then
d.text = ch_convert_cached(d.text)
table.insert(all_danmaku, d)
else
-- msg.debug("命中黑名单: " .. pattern)
end
end
else
msg.info("无法读取文件内容: " .. DANMAKU_PATH)
end
else
msg.info("文件不存在: " .. DANMAKU_PATH)
end
end
if #all_danmaku == 0 then
msg.info("未能解析任何弹幕")
return nil
end
if options.max_screen_danmaku > 0 and options.merge_tolerance <= 0 then
options.merge_tolerance = options.scrolltime
end
-- 按时间排序
table.sort(all_danmaku, function(a, b)
return a.time < b.time
end)
all_danmaku = merge_duplicate_danmaku(all_danmaku, options.merge_tolerance)
return all_danmaku
end
--# 弹幕数组与布局算法 (Danmaku Array & Layout Algorithms)
local DanmakuArray = {}
DanmakuArray.__index = DanmakuArray
function DanmakuArray:new(res_x, res_y, font_size)
local obj = {
solution_y = res_y,
font_size = font_size,
rows = math.floor(res_y / font_size),
time_length_array = {}
}
for i = 1, obj.rows do
obj.time_length_array[i] = { time = -1, length = 0 }
end
setmetatable(obj, self)
return obj
end
function DanmakuArray:set_time_length(row, time, length)
if row > 0 and row <= self.rows then
self.time_length_array[row] = { time = time, length = length }
end
end
function DanmakuArray:get_time(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].time
end
return -1
end
function DanmakuArray:get_length(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].length
end
return 0
end
-- 滚动弹幕 Y 坐标算法
function get_position_y(font_size, appear_time, text_length, resolution_x, roll_time, array)
local velocity = (text_length + resolution_x) / roll_time
local best_row = 0
local best_bias = -math.huge
for i = 1, array.rows do
local previous_appear_time = array:get_time(i)
if array:get_time(i) < 0 then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
local previous_length = array:get_length(i)
local previous_velocity = (previous_length + resolution_x) / roll_time
local delta_velocity = velocity - previous_velocity
local delta_x = (appear_time - previous_appear_time) * previous_velocity - (previous_length + text_length) / 2
if delta_x >= 0 then
if delta_velocity <= 0 then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
local delta_time = delta_x / delta_velocity
local bias = appear_time - previous_appear_time - delta_time
-- 判断:追及点是否在屏幕之外
local t_catch = previous_appear_time + delta_time
local distance_prev = previous_velocity * (t_catch - previous_appear_time)
if distance_prev > resolution_x then
-- 追及发生在屏幕之外,允许放置
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
if bias > 0 then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
elseif bias > best_bias then
best_bias = bias
best_row = i
end
end
end
-- 所有行都被占用,放弃渲染
return nil
end
-- 固定弹幕 Y 坐标算法
function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
local best_row = 0
local best_bias = -1
local row_start, row_end, row_step
if from_top then
row_start, row_end, row_step = 1, array.rows, 1
else
row_start, row_end, row_step = array.rows, 1, -1
end
for i = row_start, row_end, row_step do
local previous_appear_time = array:get_time(i)
if previous_appear_time < 0 then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
else
local delta_time = appear_time - previous_appear_time
if delta_time > fixtime then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
elseif delta_time > best_bias then
best_bias = delta_time
best_row = i
end
end
end
-- 所有行都被占用,放弃渲染
return nil
end
-- 将弹幕转换为 ASS 格式
function convert_danmaku_to_ass(all_danmaku, danmaku_file)
if #all_danmaku == 0 then
msg.info("弹幕文件为空或解析失败")
return false
end
msg.info("已解析 " .. #all_danmaku .. " 条弹幕")
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local bold = options.bold and "1" or "0"
local fontsize = tonumber(options.fontsize) or 50
local scrolltime = tonumber(options.scrolltime) or 15
local fixtime = tonumber(options.fixtime) or 5
local outline = tonumber(options.outline) or 1.0
local shadow = tonumber(options.shadow) or 0.0
local res_x = 1920
local res_y = 1080
local roll_array = DanmakuArray:new(res_x, res_y, fontsize)
local top_array = DanmakuArray:new(res_x, res_y, fontsize)
local ass_header = string.format([[
[Script Info]
Title: DanmakuConvert for mpv
ScriptType: v4.00+
Collisions: Normal
PlayResX: %d
PlayResY: %d
Timer: 100.0000
WrapStyle: 2
ScaledBorderAndShadow: yes
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: R2L,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,7,0,0,0,1
Style: TOP,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,8,0,0,0,1
Style: BTM,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,2,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
]], res_x, res_y, options.fontname, fontsize, alpha, alpha, bold, outline, shadow,
options.fontname, fontsize, alpha, alpha, bold, outline, shadow,
options.fontname, fontsize, alpha, alpha, bold, outline, shadow)
-- 预处理弹幕,先计算时间段以便进行数量限制
local pre_events = {}
for _, d in ipairs(all_danmaku) do
local time = d.type == 1 and math.floor(d.time + 0.5) or d.time
local appear_time = time
local danmaku_type = d.type
local end_time = nil
if danmaku_type >= 1 and danmaku_type <= 3 then
end_time = appear_time + scrolltime
elseif danmaku_type == 5 or danmaku_type == 4 then
end_time = appear_time + fixtime
end
if end_time then
table.insert(pre_events, {start_time = appear_time, end_time = end_time, danmaku = d})
end
end
if options.max_screen_danmaku > 0 then
pre_events = limit_danmaku(pre_events, options.max_screen_danmaku)
end
local ass_events = {}
for _, ev in ipairs(pre_events) do
local d = ev.danmaku
local appear_time = ev.start_time
local danmaku_type = d.type
local text = ass_escape(decode_html_entities(d.text))
:gsub("x(%d+)$", "{\\b1\\i1}x%1")
-- 颜色从十进制转为 BGR Hex
local color = math.max(0, math.min(d.color or 0xFFFFFF, 0xFFFFFF))
local color_hex = string.format("%06X", color)
local r = string.sub(color_hex, 1, 2)
local g = string.sub(color_hex, 3, 4)
local b = string.sub(color_hex, 5, 6)
local color_text = string.format("{\\c&H%s%s%s&}", b, g, r)
local start_time_str = seconds_to_time(appear_time)
local layer, end_time_str, style, effect
-- 滚动弹幕 (类型 1, 2, 3)
if danmaku_type >= 1 and danmaku_type <= 3 then
layer = 0
end_time_str = seconds_to_time(ev.end_time)
style = "R2L"
local text_length = get_str_width(text, fontsize)
local x1 = res_x + text_length / 2
local x2 = -text_length / 2
local y = get_position_y(fontsize, appear_time, text_length, res_x, scrolltime, roll_array)
if y then
effect = string.format("{\\move(%d, %d, %d, %d)}", x1, y, x2, y)
end
-- 顶部弹幕 (类型 5)
elseif danmaku_type == 5 then
layer = 1
end_time_str = seconds_to_time(ev.end_time)
style = "TOP"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, true)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
end
-- 底部弹幕 (类型 4)
elseif danmaku_type == 4 then
layer = 1
end_time_str = seconds_to_time(ev.end_time)
style = "BTM"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, false)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
end
end
if style then
local line = nil
if effect then
line = string.format("Dialogue: %d,%s,%s,%s,,0,0,0,,%s%s%s", layer, start_time_str, end_time_str, style, effect, color_text, text)
else
line = string.format("Comment: %d,%s,%s,%s,,0,0,0,,%s%s", layer, start_time_str, end_time_str, style, color_text, text)
end
table.insert(ass_events, line)
end
end
local final_ass = ass_header .. table.concat(ass_events, "\n")
local ass_file = io.open(danmaku_file, "w")
if not ass_file then
msg.info("错误: 无法写入 ASS 弹幕文件")
return false
end
ass_file:write(final_ass)
ass_file:close()
msg.debug("已成功转换并写入 ASS" .. danmaku_file)
return true
end
-- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_input, danmaku_out, delays)
local all_danmaku = parse_danmaku_files(danmaku_input, delays)
if not all_danmaku then
show_message("转换 XML 弹幕失败", 3)
msg.info("转换 XML 弹幕失败")
return
end
-- 拼接为 XML 内容
local xml = { '<?xml version="1.0" encoding="UTF-8"?><i>\n' }
for _, d in ipairs(all_danmaku) do
local time = d.time
local type = d.type or 1
local size = d.size or 25
local color = d.color or 0xFFFFFF
local text = d.text or ""
text = text:gsub("&", "&amp;")
:gsub("<", "&lt;")
:gsub(">", "&gt;")
:gsub("\"", "&quot;")
:gsub("'", "&apos;")
table.insert(xml, string.format('<d p="%s,%s,%s,%s">%s</d>\n', time, type, size, color, text))
end
table.insert(xml, '</i>')
-- 写入 XML 文件
local file = io.open(danmaku_out, "w")
if not file then
show_message("无法写入目标 XML 文件", 3)
msg.info("无法写入目标 XML 文件: " .. danmaku_out)
return false
end
file:write(table.concat(xml))
file:close()
show_message("转换 XML 弹幕成功: " .. danmaku_out, 3)
msg.info("转换 XML 弹幕成功: " .. danmaku_out)
return true
end
-- 解析和转换弹幕
function convert_danmaku_format(danmaku_input, danmaku_file, delays)
local all_danmaku = parse_danmaku_files(danmaku_input, delays)
if all_danmaku then
convert_danmaku_to_ass(all_danmaku, danmaku_file)
else
msg.info("未能解析对应的 .xml 或 .json 弹幕文件")
return false
end
end
+296
View File
@@ -0,0 +1,296 @@
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
local msg = require('mp.msg')
local utils = require("mp.utils")
local INTERVAL = options.vf_fps and 0.01 or 0.001
local osd_width, osd_height, pause = 0, 0, true
-- 提取 \move 参数 (x1, y1, x2, y2) 并返回
local function parse_move_tag(text)
-- 匹配包括小数和负数在内的坐标值
local x1, y1, x2, y2 = text:match("\\move%((%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
if x1 and y1 and x2 and y2 then
return tonumber(x1), tonumber(y1), tonumber(x2), tonumber(y2)
end
return nil
end
local function parse_comment(event, pos, height, delay)
local x1, y1, x2, y2 = parse_move_tag(event.text)
local displayarea = tonumber(height * options.displayarea)
if not x1 then
local current_x, current_y = event.text:match("\\pos%((%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
if not current_y or tonumber(current_y) > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\an8}%s", event.text)
else
return string.format("{\\an7}%s", event.text)
end
end
-- 计算移动的时间范围
local duration = event.end_time - event.start_time --mean: options.scrolltime
local progress = (pos - event.start_time - delay) / duration -- 移动进度 [0, 1]
-- 计算当前坐标
local current_x = tonumber(x1 + (x2 - x1) * progress)
local current_y = tonumber(y1 + (y2 - y1) * progress)
-- 移除 \move 标签并应用当前坐标
local clean_text = event.text:gsub("\\move%(.-%)", "")
if current_y > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\pos(%.1f,%.1f)\\an8}%s", current_x, current_y, clean_text)
else
return string.format("{\\pos(%.1f,%.1f)\\an7}%s", current_x, current_y, clean_text)
end
end
-- 从 ASS 文件中解析样式和事件
local function parse_ass_events(ass_path, callback)
local ass_file = io.open(ass_path, "r")
if not ass_file then
callback("无法打开 ASS 文件")
return
end
local events = {}
local time_tolerance = options.merge_tolerance
for line in ass_file:lines() do
if line:match("^Dialogue:") then
local start_time, end_time, style, text = line:match("Dialogue:%s*[^,]*,%s*([^,]*),%s*([^,]*),%s*([^,]*),[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,(.*)")
if start_time and end_time and text then
local event = {
start_time = time_to_seconds(start_time),
end_time = time_to_seconds(end_time),
style = style,
text = text:gsub("%s+$", ""),
clean_text = text:gsub("\\h+", " "):gsub("{[\\=].-}", ""):gsub("^%s*(.-)%s*$", "%1"),
pos = text:match("\\pos"),
move = text:match("\\move"),
}
table.insert(events, event)
end
end
end
table.sort(events, function(a, b)
return a.start_time < b.start_time
end)
ass_file:close()
callback(nil, events)
end
local overlay = mp.create_osd_overlay('ass-events')
function render()
if COMMENTS == nil then return end
local pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
local delay = get_delay_for_time(DELAYS, pos)
local fontname = options.fontname
local fontsize = options.fontsize
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
fontsize = options.fontsize - ratio * 2
end
local ass_events = {}
for _, event in ipairs(COMMENTS) do
if pos >= event.start_time + delay and pos <= event.end_time + delay then
local text = parse_comment(event, pos, height, delay)
if text then
text = text:gsub("&#%d+;","")
end
if text and text:match("\\fs%d+") then
text = text:gsub("\\fs(%d+)", function(size)
return string.format("\\fs%d", size * 1.5)
end)
end
-- 构建 ASS 字符串
local ass_text = text and string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}%s",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0", text)
table.insert(ass_events, ass_text)
end
end
overlay.res_x = width
overlay.res_y = height
overlay.data = table.concat(ass_events, '\n')
overlay:update()
end
local timer = mp.add_periodic_timer(INTERVAL, render, true)
function parse_danmaku(ass_file_path, from_menu, no_osd)
parse_ass_events(ass_file_path, function(err, events)
COMMENTS = events
if err then
msg.error("ASS 解析错误: " .. err)
return
end
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
end
end)
end
local function filter_state(label, name)
local filters = mp.get_property_native("vf")
for _, filter in pairs(filters) do
if filter.label == label or filter.name == name
or filter.params[name] ~= nil then
return true
end
end
return false
end
function show_danmaku_func()
render()
if not pause then
timer:resume()
end
if options.vf_fps then
local display_fps = mp.get_property_number('display-fps')
local video_fps = mp.get_property_number('estimated-vf-fps')
if (display_fps and display_fps < 58) or (video_fps and video_fps > 58) then
return
end
if not filter_state("danmaku", "fps") then
mp.commandv("vf", "append", string.format("@danmaku:fps=fps=%s", options.fps))
end
end
end
function hide_danmaku_func()
timer:kill()
overlay:remove()
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
end
end
local message_overlay = mp.create_osd_overlay('ass-events')
local message_timer = mp.add_timeout(3, function()
message_overlay:remove()
end, true)
function show_message(text, time)
message_timer.timeout = time or 3
message_timer:kill()
message_overlay:remove()
local message = string.format("{\\an%d\\pos(%d,%d)}%s", options.message_anlignment,
options.message_x, options.message_y, text)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
end
message_overlay.res_x = width
message_overlay.res_y = height
message_overlay.data = message
message_overlay:update()
message_timer:resume()
end
mp.observe_property('osd-width', 'number', function(_, value) osd_width = value or osd_width end)
mp.observe_property('osd-height', 'number', function(_, value) osd_height = value or osd_height end)
mp.observe_property('display-fps', 'number', function(_, value)
if value ~= nil then
local interval = 1 / value / 10
if interval > INTERVAL then
timer:kill()
timer = mp.add_periodic_timer(interval, render, true)
if ENABLED then
timer:resume()
end
else
timer:kill()
timer = mp.add_periodic_timer(INTERVAL, render, true)
if ENABLED then
timer:resume()
end
end
end
end)
mp.observe_property('pause', 'bool', function(_, value)
if value ~= nil then
pause = value
end
if ENABLED then
if pause then
timer:kill()
elseif COMMENTS ~= nil then
timer:resume()
end
end
end)
mp.register_event('playback-restart', function(event)
if event.error then
return msg.error(event.error)
end
if ENABLED and COMMENTS ~= nil then
render()
end
end)
mp.add_hook("on_unload", 50, function()
COMMENTS, DELAY = nil, 0
timer:kill()
overlay:remove()
mp.set_property_native(DELAY_PROPERTY, 0)
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
end
local files_to_remove = {
file1 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".json"),
file2 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".ass"),
file3 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
file4 = utils.join_path(DANMAKU_PATH, "bahamut-" .. PID .. ".json")
}
if options.save_danmaku and file_exists(files_to_remove.file2) then
save_danmaku(true)
end
for _, file in pairs(files_to_remove) do
if file_exists(file) then
os.remove(file)
end
end
for _, source in pairs(DANMAKU.sources) do
if source.fname and source.from and source.from ~= "user_local" and file_exists(source.fname) then
os.remove(source.fname)
end
end
DANMAKU = {sources = {}, count = 1}
end)
+53
View File
@@ -0,0 +1,53 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
local repo = "Tony15246/uosc_danmaku"
local local_version = VERSION or "0.0.0"
local function version_greater(v1, v2)
local function parse(ver)
local a, b, c = ver:match("v?(%d+)%.(%d+)%.(%d+)")
return tonumber(a), tonumber(b), tonumber(c)
end
local a1, a2, a3 = parse(v1)
local b1, b2, b3 = parse(v2)
if a1 ~= b1 then return a1 > b1 end
if a2 ~= b2 then return a2 > b2 end
return a3 > b3
end
local function get_latest_release(repo)
local url = "https://api.github.com/repos/" .. repo .. "/releases/latest"
local cmd = { "curl", "-sL", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then return nil end
local tag = res.stdout:match([["tag_name"%s*:%s*"([^"]+)"]])
return tag
end
-- 仅检查并提示新版本,不自动下载/覆盖(避免 rm -rf 破坏配置)
function check_for_update()
local latest_version = get_latest_release(repo)
if not latest_version then
show_message("无法获取最新版本信息")
msg.warn("无法获取最新版本信息")
return
end
if not version_greater(latest_version, local_version) then
show_message("uosc_danmaku 已是最新版本 (" .. local_version .. ")")
msg.info("uosc_danmaku 已是最新版本 (" .. local_version .. ")")
return
end
local update_url = "https://github.com/" .. repo .. "/releases/tag/" .. latest_version
show_message("uosc_danmaku 有新版本: " .. latest_version .. " (当前: " .. local_version .. ")\n请手动更新: " .. update_url)
msg.info("uosc_danmaku 新版本: " .. latest_version .. " 下载地址: " .. update_url)
end
+664
View File
@@ -0,0 +1,664 @@
local utils = require("mp.utils")
-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
-- return a substring based on utf8 characters
-- like string.sub, but negative index is not supported
function utf8_sub(s, i, j)
if i > j then
return s
end
local t, idx = {}, 1
for char in s:gmatch(UTF8_PATTERN) do
if idx >= i and idx <= j then
t[#t + 1] = char
end
idx = idx + 1
end
return table.concat(t)
end
function utf8_len(s)
local count = 0
for _ in s:gmatch(UTF8_PATTERN) do
count = count + 1
end
return count
end
function utf8_iter(s)
local iter = s:gmatch(UTF8_PATTERN)
return function()
return iter()
end
end
function utf8_to_table(s)
local t = {}
for ch in utf8_iter(s) do
t[#t + 1] = ch
end
return t
end
-- abbreviate string if it's too long
function abbr_str(str, length)
if not str or str == '' then return '' end
local str_clip = utf8_sub(str, 1, length)
if str ~= str_clip then
return str_clip .. '...'
end
return str
end
function get_str_width(text, font_size)
local width = 0
for i = 1, #text do
local byte = string.byte(text, i)
if byte > 127 then
width = width + 2
else
width = width + 1
end
end
local unicode_width = 0
local i = 1
while i <= #text do
local byte = string.byte(text, i)
local char_len
if byte < 128 then char_len = 1; unicode_width = unicode_width + 1
elseif byte >= 192 and byte < 224 then char_len = 2; unicode_width = unicode_width + 2
elseif byte >= 224 and byte < 240 then char_len = 3; unicode_width = unicode_width + 2
elseif byte >= 240 and byte < 248 then char_len = 4; unicode_width = unicode_width + 2
else char_len = 1; unicode_width = unicode_width + 1
end
i = i + char_len
end
return unicode_width * (font_size / 2)
end
function unicode_to_utf8(unicode)
if unicode < 0x80 then
return string.char(unicode)
else
local byte_count
if unicode < 0x800 then
byte_count = 2
elseif unicode < 0x10000 then
byte_count = 3
elseif unicode < 0x110000 then
byte_count = 4
else
return
end
local res = {}
local shift = 2 ^ 6
local after_shift = unicode
for _ = byte_count, 2, -1 do
local before_shift = after_shift
after_shift = math.floor(before_shift / shift)
table.insert(res, 1, before_shift - after_shift * shift + 0x80)
end
shift = 2 ^ (8 - byte_count)
table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift)
---@diagnostic disable-next-line: deprecated
return string.char(unpack(res))
end
end
function jaro(s1, s2)
local match_window = math.floor(math.max(#s1, #s2) / 2.0) - 1
local matches1 = {}
local matches2 = {}
local m = 0;
local t = 0;
for i = 0, #s1, 1 do
local start = math.max(0, i - match_window)
local final = math.min(i + match_window + 1, #s2)
for k = start, final, 1 do
if not (matches2[k] or s1[i] ~= s2[k]) then
matches1[i] = true
matches2[k] = true
m = m + 1
break
end
end
end
if m == 0 then
return 0.0
end
local k = 0
for i = 0, #s1, 1 do
if matches1[i] then
while not matches2[k] do
k = k + 1
end
if s1[i] ~= s2[k] then
t = t + 1
end
k = k + 1
end
end
t = t / 2.0
return (m / #s1 + m / #s2 + (m - t) / m) / 3.0
end
function jaro_winkler(s1, s2)
if #s1 + #s2 == 0 then
return 0.0
end
if s1 == s2 then
return 1.0
end
s1 = utf8_to_table(s1)
s2 = utf8_to_table(s2)
local d = jaro(s1, s2)
local p = 0.1
local l = 0;
while (s1[l] == s2[l] and l < 4) do
l = l + 1
end
return d + l * p * (1 - d)
end
-- 从时间字符串转换为秒数
function time_to_seconds(time_str)
local h, m, s = time_str:match("(%d+):(%d+):([%d%.]+)")
return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s)
end
-- 从秒数转换为时间字符串
function seconds_to_time(seconds)
local hours = math.floor(seconds / 3600)
local minutes = math.floor((seconds % 3600) / 60)
local secs = math.floor(seconds % 60)
local centiseconds = math.floor((seconds - math.floor(seconds)) * 100)
return string.format("%d:%02d:%02d.%02d", hours, minutes, secs, centiseconds)
end
function is_chinese(str)
return string.match(str, "[\228-\233][\128-\191]") ~= nil
end
function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end
function hex_to_bin(hexstr)
return (hexstr:gsub('..', function (cc)
return string.char(tonumber(cc, 16))
end))
end
function hex_to_char(x)
return string.char(tonumber(x, 16))
end
-- url编码转换
function url_encode(str)
-- 将非安全字符转换为百分号编码
if str then
str = str:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
return str
end
-- url解码转换
function url_decode(str)
if str ~= nil then
str = str:gsub('^%a[%a%d-_]+://', '')
:gsub('^%a[%a%d-_]+:\\?', '')
:gsub('%%(%x%x)', hex_to_char)
if str:find('://localhost:?') then
str = str:gsub('^.*/', '')
end
str = str:gsub('%?.+', '')
:gsub('%+', ' ')
return str
end
end
-- Utility function to split a string by a delimiter
function split(str, delim)
local result = {}
for match in (str .. delim):gmatch("(.-)" .. delim) do
table.insert(result, match)
end
return result
end
function table_to_zero_indexed(tbl)
for i = #tbl, 1, -1 do
tbl[i - 1] = tbl[i]
end
tbl[#tbl] = nil
return tbl
end
function itable_index_of(itable, value)
for index = 1, #itable do
if itable[index] == value then
return index
end
end
end
function is_nested_table(t)
if type(t) ~= "table" then
return false
end
for _, v in pairs(t) do
if type(v) == "table" then
return true
end
end
return false
end
function shallow_copy(original)
if type(original) ~= "table" then
return original
end
local copy = {}
for k, v in pairs(original) do
copy[k] = v
end
return copy
end
function deep_copy(obj, seen)
if type(obj) ~= "table" then
return obj
end
if seen and seen[obj] then
return seen[obj]
end
local s = seen or {}
local copy = {}
s[obj] = copy
for k, v in pairs(obj) do
copy[deep_copy(k, s)] = deep_copy(v, s)
end
setmetatable(copy, getmetatable(obj))
return copy
end
function remove_query(url)
local qpos = string.find(url, "?", 1, true)
if qpos then
return string.sub(url, 1, qpos - 1)
else
return url
end
end
function file_exists(path)
if path then
local meta = utils.file_info(path)
return meta and meta.is_file
end
return false
end
function is_writable(path)
local file = io.open(path, "w")
if file then
file:close()
os.remove(path)
return true
end
return false
end
function contains_any(tab, val)
for _, element in pairs(tab) do
if string.find(val, element) then
return true
end
end
return false
end
--读history 和 写history
function read_file(file_path)
local file = io.open(file_path, "r") -- 打开文件,"r"表示只读模式
if not file then
return nil
end
local content = file:read("*all") -- 读取文件所有内容
file:close() -- 关闭文件
return content
end
-- 应用额外的自定义标题替换规则
function title_replace(title)
local title_replace = utils.parse_json(options.title_replace)
if not title_replace then
return title
end
for _, v in pairs(title_replace) do
for _, indexrules in pairs(v['rules']) do
for rule, override in pairs(indexrules) do
title = title:gsub(rule, override)
:gsub("[_%.]", " ")
:gsub("^%s*(.-)%s*$", "%1")
:gsub("[@#%.%+%-%%&*_=,/~`]+$", "")
end
end
end
return title
end
function write_json_file(file_path, data)
local file = io.open(file_path, "w")
if not file then
return
end
file:write(utils.format_json(data)) -- 将 Lua 表转换为 JSON 并写入
file:close()
end
-- 拆分字符串中的字符和数字
local function split_by_numbers(filename)
local parts = {}
local pattern = "([^%d]*)(%d+)([^%d]*)"
for pre, num, post in string.gmatch(filename, pattern) do
table.insert(parts, {pre = pre, num = tonumber(num), post = post})
end
return parts
end
-- 识别并匹配前后剧集
local function compare_filenames(fname1, fname2)
local parts1 = split_by_numbers(fname1)
local parts2 = split_by_numbers(fname2)
local min_len = math.min(#parts1, #parts2)
-- 逐个部分进行比较
for i = 1, min_len do
local part1 = parts1[i]
local part2 = parts2[i]
-- 比较数字前的字符是否相同
if part1.pre ~= part2.pre then
return false
end
-- 比较数字部分
if part1.num ~= part2.num then
return part1.num, part2.num
end
-- 比较数字后的字符是否相同
if part1.post ~= part2.post then
return false
end
end
return false
end
-- 规范化路径
function normalize(path)
if normalize_path ~= nil then
if normalize_path then
path = mp.command_native({"normalize-path", path})
else
local directory = mp.get_property("working-directory", "")
path = utils.join_path(directory, path:gsub('^%.[\\/]',''))
if PLATFORM == "windows" then path = path:gsub("\\", "/") end
end
return path
end
normalize_path = false
local commands = mp.get_property_native("command-list", {})
for _, command in ipairs(commands) do
if command.name == "normalize-path" then
normalize_path = true
break
end
end
return normalize(path)
end
-- 获取父目录路径
function get_parent_directory(path)
local dir = nil
if path and not is_protocol(path) then
path = normalize(path)
dir = utils.split_path(path)
end
return dir
end
-- 获取播放文件标题信息
function parse_title()
local path = mp.get_property("path")
local filename = mp.get_property("filename/no-ext")
if not filename then
return
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local media_title, season, episode = nil, nil, nil
if path and not is_protocol(path) then
local title = format_filename(filename)
if title then
media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return title_replace(media_title), season, episode
else
media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return title_replace(media_title), season, episode
end
end
return title_replace(title)
end
local directory = get_parent_directory(path)
local dir, title = utils.split_path(directory:sub(1, -2))
if title:lower():match("^%s*seasons?%s*%d+%s*$") or title:lower():match("^%s*specials?%s*$") or title:match("^%s*SPs?%s*$")
or title:match("^%s*O[VAD]+s?%s*$") or title:match("^%s*第.-[季部]+%s*$") then
directory, title = utils.split_path(dir:sub(1, -2))
end
title = title
:gsub(thin_space, " ")
:gsub("%[.-%]", "")
:gsub("^%s*%(%d+.?%d*.?%d*%)", "")
:gsub("%(%d+.?%d*.?%d*%)%s*$", "")
:gsub("[%._]", " ")
:gsub("^%s*(.-)%s*$", "%1")
return title_replace(title)
end
local title = mp.get_property("media-title")
if title then
title = title:gsub(thin_space, " ")
local ftitle = url_decode(title)
local name, class = ftitle:match("^(.-)%s*|%s*(.-)%s*$")
if name then ftitle = name end
local format_title = format_filename(ftitle)
if format_title then
media_title, season, episode = format_title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
title = media_title
else
media_title, episode = format_title:match("^(.-)%s*[eE](%d+)")
if episode then
season = 1
title = media_title
else
title = format_title
end
end
end
end
return title_replace(title), season, episode
end
-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
-- 尝试对比记录文件名来获取当前集数
if fname then
local episode_num1, episode_num2 = compare_filenames(fname, filename)
if episode_num1 and episode_num2 then
return episode_num1, episode_num2
else
return nil, nil
end
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local title = format_filename(filename)
if title then
local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return tonumber(episode)
else
local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return tonumber(episode)
end
end
end
return nil
end
local CHINESE_NUM_MAP = {
[""] = 0, [""] = 1, [""] = 2, [""] = 3, [""] = 4,
[""] = 5, [""] = 6, [""] = 7, [""] = 8, [""] = 9,
[""] = 10, [""] = 100, [""] = 1000, [""] = 10000,
}
function chinese_to_number(cn)
local total = 0
local num = 0
local unit = 1
local chars = {}
for uchar in cn:gmatch(UTF8_PATTERN) do
table.insert(chars, 1, uchar)
end
for _, char in ipairs(chars) do
local val = CHINESE_NUM_MAP[char]
if val then
if val >= 10 then
if num == 0 then
num = 1
end
unit = val
else
total = total + val * unit
unit = 1
num = 0
end
end
end
if unit > 1 then
total = total + num * unit
end
if total > 0 then
return total
else
return num
end
end
local CHINESE_NUM = {"", "", "", "", "", "", "", "", "", ""}
local CHINESE_UNIT = {"", "", "", ""}
local CHINESE_BIG_UNIT = {"", "", "亿"}
function number_to_chinese(num)
if num == 0 then return "" end
local str = tostring(num)
local len = #str
local result = ""
local zero_flag = false
for i = 1, len do
local digit = tonumber(str:sub(i, i))
local pos = len - i + 1
local small_unit_index = (pos - 1) % 4 + 1
local small_unit = CHINESE_UNIT[small_unit_index]
if digit == 0 then
zero_flag = true
else
if zero_flag then
result = result .. ""
zero_flag = false
end
if digit == 1 and small_unit_index == 2 and i == 1 then
result = result .. small_unit
else
result = result .. CHINESE_NUM[digit + 1] .. small_unit
end
end
if pos % 4 == 1 and pos > 1 then
local big_unit_index = math.floor((pos - 1) / 4)
result = result .. CHINESE_BIG_UNIT[big_unit_index + 1]
end
end
result = result:gsub("零+$", "")
return result
end
-- 异步执行命令
-- 同时返回 abort 函数,用于取消异步命令
function call_cmd_async(args, callback)
async_running = true
local abort_signal = mp.command_native_async({
name = 'subprocess',
capture_stderr = true,
capture_stdout = true,
playback_only = false,
args = args,
}, function(success, result, error)
if not success or not result or result.status ~= 0 then
local exit_code = (result and result.status or 'unknown')
local message = error or (result and result.stdout .. result.stderr) or ''
callback('Calling failed. Exit code: ' .. exit_code .. ' Error: ' .. message, {})
return
end
local json = result and type(result.stdout) == 'string' and result.stdout or ''
return callback(nil, json)
end)
return function()
mp.abort_async_command(abort_signal)
end
end