Initial commit

This commit is contained in:
Rich
2023-12-06 14:24:46 +00:00
commit edf0f5e06a
53 changed files with 3665 additions and 0 deletions

61
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,61 @@
{
"version": "0.2.0",
"configurations": [{
"name": "Python",
"type": "python",
"request": "launch",
"stopOnEntry": false,
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/mfe.py",
"cwd": "${workspaceRoot}",
"env": {},
"envFile": "${workspaceRoot}/.env",
"console": "internalConsole",
"debugOptions": [
"RedirectOutput"
]
},
{
"name": "Integrated Terminal/Console",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"python": "${command:python.interpreterPath}",
"program": "${file}",
"cwd": "",
"console": "integratedTerminal",
"env": {},
"envFile": "${workspaceRoot}/.env",
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit"
]
},
{
"name": "External Terminal/Console",
"type": "python",
"request": "launch",
"stopOnEntry": true,
"python": "${command:python.interpreterPath}",
"program": "${file}",
"cwd": "",
"console": "externalTerminal",
"env": {},
"envFile": "${workspaceRoot}/.env",
"debugOptions": [
"WaitOnAbnormalExit",
"WaitOnNormalExit"
]
},
{
"name": "Attach (Remote Debug)",
"type": "python",
"request": "attach",
"localRoot": "${workspaceRoot}",
"remoteRoot": "${workspaceRoot}",
"port": 3000,
"secret": "my_secret",
"host": "localhost"
}
]
}

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"black-formatter.args": [
"--line-length=120"
]
}

25
Pipfile Normal file
View File

@@ -0,0 +1,25 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
configobj = ">=5.0.6"
appdirs = ">=1.4.3"
lxml = ">=3.8.0"
titlecase = ">=0.11.0"
pygame = "*"
#pyvidplayer2 = {editable = true,git = "https://github.com/ree1261/pyvidplayer2.git"}
[dev-packages]
pylint = "*"
flake8 = "*"
#cx-freeze = {editable = true,git = "https://github.com/anthony-tuininga/cx_Freeze.git"}
yapf = "*"
black = "*"
[requires]
python_version = "3.11"
[pipenv]
allow_prereleases = true

391
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,391 @@
{
"_meta": {
"hash": {
"sha256": "312e9824c037c11e65b606532a2a8e3b0ea7e3eadcae2249c2f6336e001caec9"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.11"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"appdirs": {
"hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"index": "pypi",
"version": "==1.4.4"
},
"configobj": {
"hashes": [
"sha256:6f704434a07dc4f4dc7c9a745172c1cad449feb548febd9f7fe362629c627a97",
"sha256:a7a8c6ab7daade85c3f329931a807c8aee750a2494363934f8ea84d8a54c87ea",
"sha256:d808d7e04e6f81fbb23d5ac2cd50e69ccbee58eaf9360eb89ede22d93216a314"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.0.8"
},
"lxml": {
"hashes": [
"sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3",
"sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d",
"sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a",
"sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120",
"sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305",
"sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287",
"sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23",
"sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52",
"sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f",
"sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4",
"sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584",
"sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f",
"sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693",
"sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef",
"sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5",
"sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02",
"sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc",
"sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7",
"sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da",
"sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a",
"sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40",
"sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8",
"sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd",
"sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601",
"sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c",
"sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be",
"sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2",
"sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c",
"sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129",
"sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc",
"sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2",
"sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1",
"sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7",
"sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d",
"sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477",
"sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d",
"sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e",
"sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7",
"sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2",
"sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574",
"sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf",
"sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b",
"sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98",
"sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12",
"sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42",
"sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35",
"sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d",
"sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce",
"sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d",
"sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f",
"sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db",
"sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4",
"sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694",
"sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac",
"sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2",
"sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7",
"sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96",
"sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d",
"sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b",
"sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a",
"sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13",
"sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340",
"sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6",
"sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458",
"sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c",
"sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c",
"sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9",
"sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432",
"sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991",
"sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69",
"sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf",
"sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb",
"sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b",
"sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833",
"sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76",
"sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85",
"sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e",
"sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50",
"sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8",
"sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4",
"sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b",
"sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5",
"sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190",
"sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7",
"sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa",
"sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0",
"sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9",
"sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0",
"sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b",
"sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5",
"sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7",
"sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.3"
},
"pygame": {
"hashes": [
"sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1",
"sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17",
"sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f",
"sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6",
"sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8",
"sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa",
"sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6",
"sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592",
"sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe",
"sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937",
"sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308",
"sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4",
"sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75",
"sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f",
"sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6",
"sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718",
"sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d",
"sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee",
"sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef",
"sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c",
"sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d",
"sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5",
"sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff",
"sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9",
"sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b",
"sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63",
"sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee",
"sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42",
"sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945",
"sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584",
"sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3",
"sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7",
"sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554",
"sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1",
"sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae",
"sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9",
"sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da",
"sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90",
"sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097",
"sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4",
"sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a",
"sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae",
"sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0",
"sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd",
"sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299",
"sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda",
"sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff",
"sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae",
"sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf",
"sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4",
"sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c",
"sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677",
"sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a",
"sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92",
"sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50",
"sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd",
"sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==2.5.2"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==1.16.0"
},
"titlecase": {
"hashes": [
"sha256:7d83a277ccbbda11a2944e78a63e5ccaf3d32f828c594312e4862f9a07f635f5"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.4.1"
}
},
"develop": {
"astroid": {
"hashes": [
"sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca",
"sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"
],
"markers": "python_full_version >= '3.8.0'",
"version": "==3.0.1"
},
"black": {
"hashes": [
"sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4",
"sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b",
"sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f",
"sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07",
"sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187",
"sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6",
"sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05",
"sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06",
"sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e",
"sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5",
"sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244",
"sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f",
"sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221",
"sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055",
"sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479",
"sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394",
"sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911",
"sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==23.11.0"
},
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
"dill": {
"hashes": [
"sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e",
"sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"
],
"markers": "python_version >= '3.11'",
"version": "==0.3.7"
},
"flake8": {
"hashes": [
"sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23",
"sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.1'",
"version": "==6.1.0"
},
"importlib-metadata": {
"hashes": [
"sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7",
"sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"
],
"markers": "python_version >= '3.8'",
"version": "==7.0.0"
},
"isort": {
"hashes": [
"sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
"sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
],
"markers": "python_full_version >= '3.8.0'",
"version": "==5.12.0"
},
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mypy-extensions": {
"hashes": [
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.0"
},
"packaging": {
"hashes": [
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
],
"markers": "python_version >= '3.7'",
"version": "==23.2"
},
"pathspec": {
"hashes": [
"sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
"sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
],
"markers": "python_version >= '3.7'",
"version": "==0.11.2"
},
"platformdirs": {
"hashes": [
"sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380",
"sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"
],
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
},
"pycodestyle": {
"hashes": [
"sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f",
"sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"
],
"markers": "python_version >= '3.8'",
"version": "==2.11.1"
},
"pyflakes": {
"hashes": [
"sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774",
"sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"
],
"markers": "python_version >= '3.8'",
"version": "==3.1.0"
},
"pylint": {
"hashes": [
"sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496",
"sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.0'",
"version": "==3.0.2"
},
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.1"
},
"tomlkit": {
"hashes": [
"sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4",
"sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"
],
"markers": "python_version >= '3.7'",
"version": "==0.12.3"
},
"yapf": {
"hashes": [
"sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b",
"sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==0.40.2"
},
"zipp": {
"hashes": [
"sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31",
"sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"
],
"markers": "python_version >= '3.8'",
"version": "==3.17.0"
}
}
}

BIN
arcade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

156
config.py Normal file
View File

@@ -0,0 +1,156 @@
import os
from appdirs import user_data_dir
from configobj import ConfigObj, flatten_errors, Section
from validate import Validator
# from utils import get_pygame_keydict, get_keys
CFGSPEC = """ResolutionX = integer(default=1024)
ResolutionY = integer(default=768)
FullScreen = boolean(default=False)
Font = string(default='microsoftsansserif')
FontSize = integer(default=13)
QuitKeyPresses = integer(min=1,default=3)
ScanLines = boolean(default=True)
Key_Up = list(default=list('K_UP'))
Key_Down = list(default=list('K_DOWN'))
Key_PgUp = list(default=list('K_LEFT','K_PAGEUP'))
Key_PgDn = list(default=list('K_RIGHT','K_PAGEDOWN'))
Key_Home = list(default=list('K_HOME'))
Key_End = list(default=list('K_END'))
Key_Select = list(default=list('K_RETURN','K_1'))
Key_GameInfo = list(default=list('K_5'))
Key_GameHistoryxP = list(default=list('K_6'))
Key_Popup = list(default=list('K_2'))
Key_ShowArtwork = list(default=list())
AlwaysChangeSnap = boolean(default=True)
Emulator = string()
FPS = integer(default=60)
[__many__]
EXE = string()
Version = string(default=None)
ShowClones = boolean(default=True)
Category = string(default='All Games')
GameAtTop = integer(min=0,default=0)
CurrentGame = integer(min=0,default=0)
EmulatorType = option('MAME','MESS','CSV',default='MAME')
StatusFilter = list(default=list())
Sort = option('Name Asc','Name Dec','Year Asc','Year Dec',default='Name Asc')
SnapDir = string(default=None)
CSVFile = string(default=None)
ArtworkDirs = list(default=list())
ShowVideoSnaps = boolean(default=True)
HideMature = boolean(default=True)
"""
appname = "MFE6"
appauthor = "RMJ"
debug = True
datadir = user_data_dir(appname, appauthor)
if debug:
datadir = os.getcwd()
if not os.path.isdir(datadir):
os.makedirs(datadir)
spec = CFGSPEC.split("\n")
cfg = ConfigObj(os.path.join(datadir, "mfe.ini"), configspec=spec, encoding="UTF8")
cfg.write()
validator = Validator()
result = cfg.validate(validator, copy=True, preserve_errors=True)
errored = False
for entry in flatten_errors(cfg, result):
section_list, key, error = entry
if key is not None:
section_list.append(key)
else:
section_list.append("[missing section]")
section_string = ", ".join(section_list)
if error is False:
error = "Missing value or section."
print(section_string, " = ", error)
errored = True
if errored:
print("Error in INI file")
exit()
cfg.write()
cfg["datadir"] = datadir
if not os.path.isfile(os.path.join(datadir, "mame_exclude.txt")):
from exclude import MAME_EXCLUDE
with open(os.path.join(datadir, "mame_exclude.txt"), "wt") as f:
f.write(MAME_EXCLUDE)
# import gfx_pygame
# keys = gfx_pygame.get_keydict()
# get_keys = gfx_pygame.get_keys
# cfg["KeyUp"] = get_keys(keys, cfg["Key_Up"])
# cfg["KeyDown"] = get_keys(keys, cfg["Key_Down"])
# cfg["KeyPgUp"] = get_keys(keys, cfg["Key_PgUp"])
# cfg["KeyPgDn"] = get_keys(keys, cfg["Key_PgDn"])
# cfg["KeyHome"] = get_keys(keys, cfg["Key_Home"])
# cfg["KeyEnd"] = get_keys(keys, cfg["Key_End"])
# cfg["KeySelect"] = get_keys(keys, cfg["Key_Select"])
# cfg["KeyGameInfo"] = get_keys(keys, cfg["Key_GameInfo"])
# cfg["KeyGameHistory"] = get_keys(keys, cfg["Key_GameHistory"])
# cfg["KeyPopup"] = get_keys(keys, cfg["Key_Popup"])
# cfg["KeyShowArtwork"] = get_keys(keys, cfg["Key_ShowArtwork"])
def config_write():
Exclude = [
"KeyUp",
"KeyDown",
"KeyPgUp",
"KeyPgDn",
"KeySelect",
"KeyGameInfo",
"KeyGameHistory",
"KeyPopup",
"KeyHome",
"KeyEnd",
"KeyShowArtwork",
"datadir",
]
t = {}
for item in Exclude:
t[item] = cfg[item]
del cfg[item]
cfg.write()
for item in t:
cfg[item] = t[item]
# def get_emu(option=None):
# if option:
# r = cfg[cfg['Emulator']][option]
# else:
# r = cfg[cfg['Emulator']]
# return r
# def set_emu(option, data):
# cfg[cfg['Emulator']][option] = data
def get_emulators():
r = []
for k in cfg.keys():
if isinstance(cfg[k], Section):
r.append(k)
return r

10
emu.py Normal file
View File

@@ -0,0 +1,10 @@
from config import cfg
import mame
class emu:
def __init__(self):
self.config = cfg[cfg["Emulator"]]
self.name = cfg["Emulator"]
if self.config["EmulatorType"].upper() == "MAME":
self.roms = mame.mameROMs(self.config, cfg["datadir"])

99
exclude.py Normal file
View File

@@ -0,0 +1,99 @@
MAME_EXCLUDE = """3D Printer
Astrological Computer
Audio Sequencer
Bank-teller Terminal
Barcode Printer
Bridge Machine
Business Computer / Terminal
Calculator / Pocket Computer
Cash Counter
Chess Machine
Clock
Credit Card Terminal
DVD Player
DVD Reader/Writer
Dame Machine
Development Computer
Devices
Document Processors
Dot-Matrix Display
Drum Machine
EPROM Programmer
Educational Game
Electromechanical / Change Money
Electromechanical / Coin Pusher
Electromechanical / Misc.
Electromechanical / Pinball
Electromechanical / Redemption
Electromechanical / Reels
Electromechanical / Utilities
Electronic Board Game
Electronic Typewriter
Engine Control Unit
Gambling Board
Game Console
Game Console Expansion
Graphic Tablet
Graphics Display Controller
Handheld Child Computers
Handheld Game
Handheld Game Console
Home Computer
In Circuit Emulator
JukeBox
Kit Computer
Laptop / Notebook / Portable
Laser Printer
Matrix Printer
Microcomputer
Misc.
Misc. * Mature *
Mobile Phone
Modem
Multi-cart Board
Network Processor
Not Classified
Pinball
Pinball * Mature *
Pinball / Pachinko
Pinball / Pachinko * Mature *
Player
Pocket Device / Pad / PDA
Portable Media Player
Print Club
Printer Handbook
Programming Machine
Punched Card Computer
Quiz / Chinese
Quiz / French
Quiz / German
Quiz / Italian
Quiz / Japanese
Quiz / Japanese * Mature *
Quiz / Japanese - Music
Quiz / Korean
Quiz / Spanish
Rhythm / Dance
Rhythm / Instruments
Rhythm / Misc.
Robot Control
Satellite Receiver
Single Board Computer
Speech Synthesizer
Synthesizer
System / BIOS
System / Device
Talking Calculator
Telephone / ComputerPhone
Test ROM
Thermal Printer
Toy cars
Training Board
Utilities / Test
Utilities / Update
VTR Control
Virtual Environment
Wavetables Generator
Word-processing Machine
Workstation / Server
"""

413
gui.py Normal file
View File

@@ -0,0 +1,413 @@
import pygame
# pylint: disable=E0611
from pygame.locals import KEYDOWN
# pylint: enable=E0611
from configfile import cfg
from utils import wrap_multi_line, image_from_data
# pylint: disable=E1121,R0902,R0903,R0912,R0913
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
class baseobject(object):
def __init__(self,
surface,
x,
y,
width,
height,
data,
font,
border=False,
fg=WHITE,
bg=BLACK,
name=None,
title=None):
self.surface = surface
self.x = x
self.y = y
self.width = width
self.height = height
self.font = font
self.border = border
self.fontheight = self.font.get_height()
self.fg = fg
self.bg = bg
self.data = data
self.name = name
self.title = title
if self.width < 1:
for item in self.data:
size = self.font.size(item)[0]
if size > self.width:
self.width = size
if self.width > self.surface.get_width():
self.surface.get_width()
if self.border:
self.width += 8
if self.height < 1:
self.height = self.fontheight * (len(self.data))
if self.border:
self.height += 8
if self.height > self.surface.get_height():
if self.border:
self.height = self.surface.get_height() - 8 - self.y
else:
self.height = self.surface.get_height - self.y
if self.x == -1:
self.x = int((self.surface.get_width() / 2) - (self.width / 2))
if self.y == -1:
self.y = int((self.surface.get_height() / 2) - (self.height / 2))
if self.border:
if not self.title:
self.textx = self.x + 4
self.texty = self.y + 4
self.textwidth = self.width - 8
self.textheight = self.height - 8
else:
self.textx = self.x + 4
self.texty = self.y + 4 + self.fontheight
self.textwidth = self.width - 8
self.textheight = self.height - 8 - self.fontheight
else:
if not self.title:
self.textx = self.x
self.texty = self.y
self.textwidth = self.width
self.textheight = self.height
else:
self.textx = self.x
self.texty = self.y + self.fontheight
self.textwidth = self.width
self.textheight = self.height - self.fontheight
class menu(baseobject):
def __init__(self,
surface,
x,
y,
width,
height,
data,
font,
border=False,
fg=WHITE,
bg=BLACK,
name=None,
startitem=0,
itemattop=0):
baseobject.__init__(self, surface, x, y, width, height, data, font,
border, fg, bg, name)
self.currentitem = startitem
self.itemattop = itemattop
self.itemsperpage = int(self.textheight / self.fontheight)
self.isrom = True
if str(type(self.data[0])) == "<class 'str'>":
self.isrom = False
def processevent(self, event): # pylint: disable=R0912
if event.type == KEYDOWN:
if event.key in cfg["KeyUp"] and self.currentitem > 0:
if self.currentitem == self.itemattop:
self.itemattop -= 1
self.currentitem -= 1
if event.key in cfg["KeyDown"] and self.currentitem < len(
self.data) - 1:
self.currentitem += 1
if self.itemattop + self.itemsperpage <= self.currentitem:
self.itemattop += 1
if event.key in cfg["KeyPgUp"] and self.currentitem > 0:
self.currentitem -= self.itemsperpage - 1
while self.itemattop > self.currentitem:
self.itemattop -= 1
if self.itemattop < 0:
self.itemattop = 0
self.currentitem = 0
if event.key in cfg["KeyPgDn"] and self.currentitem < len(
self.data) - 1:
self.currentitem += self.itemsperpage - 1
while self.currentitem - self.itemattop >= self.itemsperpage:
self.itemattop += 1
if self.itemattop + self.itemsperpage > len(self.data):
self.itemattop = len(self.data) - self.itemsperpage
self.currentitem = len(self.data) - 1
if event.key in cfg["KeyHome"] and self.currentitem > 0:
self.currentitem = 0
self.itemattop = 0
if event.key in cfg["KeyEnd"] and self.currentitem < len(
self.data) - 1:
self.currentitem = len(self.data) - 1
self.itemattop = self.currentitem - self.itemsperpage + 1
def render(self):
pygame.Surface.fill(
self.surface,
self.bg,
rect=pygame.Rect(self.x, self.y, self.width, self.height))
if self.border:
pygame.draw.rect(self.surface, self.fg,
pygame.Rect(self.x + 1, self.y + 1,
self.width - 2, self.height - 2), 1)
for i in range(0, self.itemsperpage):
if i > len(self.data) - 1:
t = pygame.Surface((self.textwidth, self.fontheight))
t.fill(self.bg)
self.surface.blit(t, (self.textx,
self.texty + i * self.fontheight))
continue
if self.isrom:
item = self.data[self.itemattop + i].description
else:
item = self.data[self.itemattop + i]
if self.itemattop + i == self.currentitem:
s = self.font.render(item, True, self.bg, self.fg)
t = pygame.Surface((self.textwidth, self.fontheight))
t.fill(self.fg)
t.blit(s, (0, 0))
self.surface.blit(t, (self.textx,
self.texty + i * self.fontheight))
else:
s = self.font.render(item, True, self.fg, self.bg)
t = pygame.Surface((self.textwidth, self.fontheight))
t.fill(self.bg)
t.blit(s, (0, 0))
self.surface.blit(t, (self.textx,
self.texty + i * self.fontheight))
class notepad(baseobject):
def __init__(self,
surface,
x,
y,
width,
height,
data,
font,
border=False,
fg=WHITE,
bg=BLACK,
name="None",
title=None):
baseobject.__init__(self, surface, x, y, width, height, data, font,
border, fg, bg, name, title)
self.data = wrap_multi_line(data, self.font, self.textwidth)
self.lineattop = 0
self.linesperpage = int(self.textheight / self.fontheight)
self.lasttopline = int(len(self.data) - self.linesperpage)
def processevent(self, event):
if event.type == KEYDOWN:
if event.key in cfg["KeyDown"] and self.lineattop < self.lasttopline:
self.lineattop += 1
elif event.key in cfg["KeyUp"] and self.lineattop > 0:
self.lineattop -= 1
elif event.key in cfg["KeyPgDn"] and self.lineattop < self.lasttopline:
self.lineattop += self.linesperpage
if self.lineattop > self.lasttopline:
self.lineattop = self.lasttopline
elif event.key in cfg["KeyPgUp"] and self.lineattop > 0:
self.lineattop -= self.linesperpage
if self.lineattop < 0:
self.lineattop = 0
elif event.key in cfg["KeyHome"] and self.lineattop > 0:
self.lineattop = 0
elif event.key in cfg["KeyEnd"] and self.lineattop < self.lasttopline:
self.lineattop = self.lasttopline
def render(self):
pygame.Surface.fill(
self.surface,
self.bg,
rect=pygame.Rect(self.x, self.y, self.width, self.height))
if self.border:
pygame.draw.rect(self.surface, self.fg,
pygame.Rect(self.x + 1, self.y + 1,
self.width - 2, self.height - 2), 1)
if self.title:
x = (self.textwidth / 2) - (
self.font.size('%s' % self.title)[0] / 2)
title_surface = pygame.Surface((self.textwidth,
self.fontheight))
title_surface.fill(self.fg)
title_surface.blit(
self.font.render('%s' % self.title, True, BLACK, self.fg),
(x, 0))
self.surface.blit(title_surface,
(self.textx, self.texty - self.fontheight))
for c in range(0, self.linesperpage):
try:
itemsurface = self.font.render(self.data[c + self.lineattop],
True, self.fg, self.bg)
self.surface.blit(itemsurface,
(self.textx,
self.texty + c * self.fontheight))
except:
pass
class image_notepad(baseobject):
def __init__(self,
surface,
x,
y,
width,
height,
data,
font,
border=False,
fg=WHITE,
bg=BLACK,
name="None",
title=None):
baseobject.__init__(self, surface, x, y, width, height, data, font,
border, fg, bg, name, title)
self.currentitem = 0
self.data = data
def processevent(self, event):
if event.type == KEYDOWN:
if event.key in cfg["KeyPgDn"]:
if self.currentitem < len(self.data) - 1:
self.currentitem += 1
elif event.key in cfg["KeyPgUp"]:
if self.currentitem > 0:
self.currentitem -= 1
def render(self):
pygame.Surface.fill(
self.surface,
self.bg,
rect=pygame.Rect(self.x, self.y, self.width, self.height))
if self.border:
pygame.draw.rect(self.surface, self.fg,
pygame.Rect(self.x + 1, self.y + 1,
self.width - 2, self.height - 2), 1)
x = (self.textwidth / 2) - (self.font.size('%s' % self.title)[0] / 2)
title_surface = pygame.Surface((self.textwidth, self.fontheight))
title_surface.fill(self.fg)
title_surface.blit(
self.font.render('%s' % self.data[self.currentitem][0], True,
BLACK, self.fg), (x, 0))
self.surface.blit(title_surface, (self.textx,
self.texty - self.fontheight))
image_surface = image_from_data(self.data[self.currentitem][1],
(self.textwidth, self.textheight))
w, h = image_surface.get_width(), image_surface.get_height()
x = (self.textwidth / 2) - (w / 2) + self.textx
y = (self.textheight / 2) - (h / 2) + self.texty
self.surface.blit(image_surface, (x, y))
class gui():
def __init__(self, surface):
self.surface = surface
self.objects = []
self.font = pygame.font.Font(
pygame.font.match_font(cfg["Font"], bold=True), cfg["FontSize"])
# self.fontheight = self.font.get_height()
self.currentobject = 0
def waitevent(self):
event = pygame.event.wait()
if self.objects:
self.objects[-1].processevent(event)
return event
def add_menu(self,
x,
y,
width,
height,
data,
border=False,
fg=WHITE,
bg=BLACK,
name="None",
startitem=0,
itemattop=0):
self.currentobject += 1
self.objects.append(
menu(self.surface, x, y, width, height, data, self.font, border,
fg, bg, name, startitem, itemattop))
def add_notepad(self,
x,
y,
width,
height,
data,
border=False,
fg=WHITE,
bg=BLACK,
name="None",
title=None):
self.currentobject += 1
self.objects.append(
notepad(self.surface, x, y, width, height, data, self.font, border,
fg, bg, name, title))
def add_image_notepad(self,
x,
y,
width,
height,
data,
border=False,
fg=WHITE,
bg=BLACK,
name="None",
title=None):
self.currentobject += 1
self.objects.append(
image_notepad(self.surface, x, y, width, height, data, self.font,
border, fg, bg, name, title))
def deletelastobject(self):
del self.objects[-1]
self.currentobject -= 1
def getmenuname(self):
return self.objects[-1].name
def deleteallmenus(self):
while self.currentobject > 1:
self.deletelastobject()
def getcurrentitem(self, objnum=None):
if objnum is None:
return self.objects[-1].currentitem
return self.objects[objnum].currentitem
def getitemattop(self):
return self.objects[-1].itemattop
# TODO: Fix me for itemattop on normal menus
def setcurrentitem(self, selected_item):
self.objects[-1].itemattop = selected_item
self.objects[-1].currentitem = selected_item
def render(self):
for t in self.objects:
t.render()
pygame.display.update()

BIN
mame.dat Normal file

Binary file not shown.

213
mame.py Normal file
View File

@@ -0,0 +1,213 @@
import os
import zipfile
import subprocess
import pickle
from lxml import etree
from config import debug
from utils import getMameResource
class mameROM(object):
def __init__(self):
self.name = ""
self.cloneof = None
self.description = ""
self.year = ""
self.manufacturer = ""
self.status = ""
self.category = ""
def __str__(self):
return self.description
def __repr__(self):
return '%s("%s", "%s", "%s", "%s", "%s", "%s", "%s")' % (
self.__class__.__name__,
self.name,
self.cloneof,
self.description,
self.year,
self.manufacturer,
self.status,
self.category,
)
class mameROMs(object):
def __init__(self, config, datadir):
self.all_roms = []
self.roms = []
self.all_categories = []
self.categories = []
self.catdict = {}
self.len = 0
self.cfg = config
self.data_dir = datadir
self.hide_mature = self.cfg["HideMature"]
self.emulator_dir = os.path.split(self.cfg["EXE"])[0]
self.parse()
self.filter()
self.snapdir = getMameResource(self.emulator_dir, "snap", is_dir=True)
self.marqueesdir = getMameResource(self.emulator_dir, "marquees", is_dir=True)
self.titlesdir = getMameResource(self.emulator_dir, "titles", is_dir=True)
self.videosnapsdir = getMameResource(self.emulator_dir, "videosnaps", is_dir=True)
self.info = getMameResource(self.emulator_dir, "mameinfo.dat", is_dir=False)
self.hostory = getMameResource(self.emulator_dir, "history.dat", is_dir=False)
def get_mame_version(self):
mamerun = subprocess.run([self.cfg["EXE"], "-?"], stdout=subprocess.PIPE)
output = mamerun.stdout.decode("utf-8")
output = output[output.find("v") :]
output = output[: output.find(" ")]
return output
def parse(self):
if not debug:
mame_version = self.get_mame_version()
print(mame_version)
xmlfile = os.path.join(self.emulator_dir, "mame.xml")
datfile = os.path.join(self.data_dir, "mame.dat")
if not debug:
if self.cfg["Version"] != mame_version:
self.cfg["Version"] = mame_version
if os.path.isfile(xmlfile):
os.unlink(xmlfile)
if os.path.isfile(datfile):
with open(datfile, "rb") as i:
temp_mame_version = pickle.load(i)
if debug or temp_mame_version == mame_version:
self.all_categories = pickle.load(i)
self.all_roms = pickle.load(i)
return True
os.unlink(datfile, "mame.dat")
tempcat = {}
catfile = open(os.path.join(self.emulator_dir, "catver.ini"))
found = False
for line in catfile:
line = line.strip()
if line == "[VerAdded]":
break
if line == "[Category]":
found = True
if found and line != "" and line[0] != ";" and line[0] != "[":
# zwackery=Platform / Run Jump
game, category = line.split("=")
tempcat[game] = category
if category not in self.all_categories:
self.all_categories.append(category)
self.catdict[category] = 0
self.all_categories.sort()
self.all_categories.insert(0, "All Games")
self.all_categories.append("Unknown")
self.catdict["All Games"] = 1
self.catdict["Unknown"] = 0
if not os.path.isfile(xmlfile):
try:
with open(xmlfile, "w") as out:
retcode = subprocess.call([self.cfg["EXE"], "-listxml"], stdout=out)
if retcode != 0:
try:
os.unlink(xmlfile)
except:
pass
return False
except OSError:
try:
os.unlink(xmlfile)
except:
pass
return False
with open(os.path.join(self.data_dir, "mame_exclude.txt"), "rt") as f:
exclude = f.readlines()
exclude = [x.strip() for x in exclude]
tree = etree.parse(xmlfile)
rom = None
for child in tree.getiterator():
if child.tag == "machine":
if rom:
if rom.category not in exclude:
self.all_roms.append(rom)
self.catdict[rom.category] += 1
rom = None
if "runnable" in child.attrib and child.attrib["runnable"] == "yes":
rom = mameROM()
rom.name = child.attrib["name"]
else:
rom = None
if rom and "cloneof" in child.attrib:
rom.cloneof = child.attrib["cloneof"]
if rom and rom.name in tempcat:
rom.category = tempcat[rom.name]
elif rom and rom.cloneof in tempcat:
rom.category = tempcat[rom.cloneof]
elif rom:
rom.category = "Unknown"
elif rom and child.tag == "description":
rom.description = child.text
elif rom and child.tag == "year":
rom.year = child.text
elif rom and child.tag == "manufacturer":
rom.manufacturer = child.text
elif rom and child.tag == "driver":
rom.status = child.attrib["status"]
if rom:
if rom.category not in exclude:
self.all_roms.append(rom)
for cat in self.catdict:
if self.catdict[cat] == 0:
del self.all_categories[self.all_categories.index(cat)]
with open(datfile, "wb") as output:
pickle.dump(mame_version, output)
pickle.dump(self.all_categories, output)
pickle.dump(self.all_roms, output)
return True
def filter(self):
self.roms = []
for rom in self.all_roms:
if rom.status in self.cfg["StatusFilter"]:
if self.cfg["Category"] == "All Games" or self.cfg["Category"] == rom.category:
if self.cfg["ShowClones"] or rom.cloneof is None:
if not self.hide_mature or "* Mature *" not in rom.category:
self.roms.append(rom)
if self.cfg["Sort"] == "Name Asc":
self.roms.sort(key=lambda x: x.description.lower(), reverse=False)
elif self.cfg["Sort"] == "Name Dec":
self.roms.sort(key=lambda x: x.description.lower(), reverse=True)
elif self.cfg["Sort"] == "Year Asc":
self.roms.sort(key=lambda x: x.year.lower(), reverse=False)
elif self.cfg["Sort"] == "Year Dec":
self.roms.sort(key=lambda x: x.year.lower(), reverse=True)
self.len = len(self.roms)
if self.hide_mature:
self.categories = []
for category in self.all_categories:
if "* Mature *" not in category:
self.categories.append(category)
else:
self.categories = self.all_categories

98
mame_exclude.txt Normal file
View File

@@ -0,0 +1,98 @@
3D Printer
Astrological Computer
Audio Sequencer
Bank-teller Terminal
Barcode Printer
Bridge Machine
Business Computer / Terminal
Calculator / Pocket Computer
Cash Counter
Chess Machine
Clock
Credit Card Terminal
DVD Player
DVD Reader/Writer
Dame Machine
Development Computer
Devices
Document Processors
Dot-Matrix Display
Drum Machine
EPROM Programmer
Educational Game
Electromechanical / Change Money
Electromechanical / Coin Pusher
Electromechanical / Misc.
Electromechanical / Pinball
Electromechanical / Redemption
Electromechanical / Reels
Electromechanical / Utilities
Electronic Board Game
Electronic Typewriter
Engine Control Unit
Gambling Board
Game Console
Game Console Expansion
Graphic Tablet
Graphics Display Controller
Handheld Child Computers
Handheld Game
Handheld Game Console
Home Computer
In Circuit Emulator
JukeBox
Kit Computer
Laptop / Notebook / Portable
Laser Printer
Matrix Printer
Microcomputer
Misc.
Misc. * Mature *
Mobile Phone
Modem
Multi-cart Board
Network Processor
Not Classified
Pinball
Pinball * Mature *
Pinball / Pachinko
Pinball / Pachinko * Mature *
Player
Pocket Device / Pad / PDA
Portable Media Player
Print Club
Printer Handbook
Programming Machine
Punched Card Computer
Quiz / Chinese
Quiz / French
Quiz / German
Quiz / Italian
Quiz / Japanese
Quiz / Japanese * Mature *
Quiz / Japanese - Music
Quiz / Korean
Quiz / Spanish
Rhythm / Dance
Rhythm / Instruments
Rhythm / Misc.
Robot Control
Satellite Receiver
Single Board Computer
Speech Synthesizer
Synthesizer
System / BIOS
System / Device
Talking Calculator
Telephone / ComputerPhone
Test ROM
Thermal Printer
Toy cars
Training Board
Utilities / Test
Utilities / Update
VTR Control
Virtual Environment
Wavetables Generator
Word-processing Machine
Workstation / Server

37
mfe.ini Normal file
View File

@@ -0,0 +1,37 @@
Emulator = MAME
ResolutionX = 500
ResolutionY = 500
FullScreen = False
Font = microsoftsansserif
FontSize = 13
QuitKeyPresses = 3
ScanLines = True
Key_Up = K_UP,
Key_Down = K_DOWN,
Key_PgUp = K_LEFT, K_PAGEUP
Key_PgDn = K_RIGHT, K_PAGEDOWN
Key_Home = K_HOME,
Key_End = K_END,
Key_Select = K_RETURN, K_1
Key_GameInfo = K_5,
Key_GameHistory = K_6,
Key_Popup = K_2,
Key_ShowArtwork = ,
AlwaysChangeSnap = True
FPS = 60
Key_GameHistoryxP = K_6,
[MAME]
EXE = /Users/rich/git/mame/mame
Version = /Users/rich/git/mame/mame
ShowClones = False
Category = All Games
GameAtTop = 0
CurrentGame = 0
EmulatorType = MAME
StatusFilter = good, imperfect
Sort = Name Asc
SnapDir = None
CSVFile = None
ArtworkDirs = cabinets, cpanel
ShowVideoSnaps = True
HideMature = True

65
mfe.py Normal file
View File

@@ -0,0 +1,65 @@
import pyglet
from config import cfg
from emu import emu
from pygletgui import pygletgui
icon = pyglet.resource.image("arcade.png")
window = pyglet.window.Window(
cfg["ResolutionX"],
cfg["ResolutionY"],
fullscreen=cfg["FullScreen"],
style=pyglet.window.Window.WINDOW_STYLE_BORDERLESS,
)
pyglet.gl.glClearColor(255, 0, 0, 1.0) # red, green, blue, and alpha(transparency)
window.set_icon(icon)
# window.set_exclusive_mouse()
font = pyglet.font.load(cfg["Font"], cfg["FontSize"])
font_height = font.ascent - font.descent + 1
print(font_height)
rom_list_width = int(cfg["ResolutionX"] / 2)
emu = emu()
gui = pygletgui(font_height)
gui.add_menu(
0,
cfg["ResolutionY"] - 1,
rom_list_width,
cfg["ResolutionY"] - 1,
border=True,
# data=self.roms,
data=["1", "2", "AAAAAaaaaAAAAAAAAaaaaaaaaaaaaasasadsfdsafdsfdsfdsfdsfdsfdsfsadfdsfsdfsdfdsfds"],
fg=(255, 255, 255),
bg=(0, 0, 0),
name="MainList",
# startitem=get_emu('CurrentGame'),
# itemattop=get_emu('GameAtTop'))
startitem=0,
itemattop=0,
font_height=font_height,
)
# # self.gui.render()
@window.event
def on_draw():
window.clear()
gui.render()
@window.event
def on_key_press(symbol, modifiers):
if symbol == pyglet.window.key.ESCAPE:
return
return pyglet.event.EVENT_HANDLED
pyglet.app.run()

10
pyvidplayer2/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
__pycache__/
build/
dist/
*.egg-info/
build.txt
test.py
test.mp4
resources/
.VSCodeCounter/
video/

21
pyvidplayer2/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 ree1261
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.

85
pyvidplayer2/README.md Normal file
View File

@@ -0,0 +1,85 @@
# pyvidplayer2 (please report all bugs!)
Introducing pyvidplayer2, the successor to pyvidplayer. It's better in
pretty much every way, and finally allows an easy and reliable way to play videos in Python.
All the features from the original library have been ported over, with the exception of ```alt_resize()```. Since pyvidplayer2 has a completely revamped foundation, the unreliability of ```set_size()``` has been quashed, and a fallback function is now redundant.
# Features (tested on Windows)
- Easy to implement (4 lines of code)
- Fast and reliable
- Adjust playback speed
- No audio/video sync issues
- Subtitle support (.srt, .ass, etc)
- Play multiple videos in parallel
- Built in GUI
- Support for Pygame, Pyglet, Tkinter, and PyQT6
- Can play all ffmpeg supported video formats
- Post process effects
- Webcam feed
# Installation
```
pip install pyvidplayer2
```
Note: FFMPEG (just the essentials is fine) must be installed and accessible via the system PATH. Here's an online article on how to do this (windows):
https://phoenixnap.com/kb/ffmpeg-windows.
# Quickstart
Refer to the examples folder for more basic guides, and documentation.md contains more detailed information.
```
import pygame
from pyvidplayer2 import Video
# create video object
vid = Video("video.mp4")
win = pygame.display.set_mode(vid.current_size)
pygame.display.set_caption(vid.name)
while vid.active:
key = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
vid.stop()
elif event.type == pygame.KEYDOWN:
key = pygame.key.name(event.key)
if key == "r":
vid.restart() #rewind video to beginning
elif key == "p":
vid.toggle_pause() #pause/plays video
elif key == "m":
vid.toggle_mute() #mutes/unmutes video
elif key == "right":
vid.seek(15) #skip 15 seconds in video
elif key == "left":
vid.seek(-15) #rewind 15 seconds in video
elif key == "up":
vid.set_volume(1.0) #max volume
elif key == "down":
vid.set_volume(0.0) #min volume
elif key == "1":
vid.set_speed(1.0) #regular playback speed
elif key == "2":
vid.set_speed(2.0) #doubles video speed
# only draw new frames, and only update the screen if something is drawn
if vid.draw(win, (0, 0), force_draw=False):
pygame.display.update()
pygame.time.wait(16) # around 60 fps
# close video when done
vid.close()
pygame.quit()
```

View File

@@ -0,0 +1,180 @@
# Video(path, chunk_size=300, max_threads=1, max_chunks=1, subs=None, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False)
Main object used to play videos. It uses FFMPEG to extract chunks of audio from videos and then feeds it into a Pyaudio stream. Finally, it uses OpenCV to display the appropriate video frames. Videos can only be played simultaneously if they're using Pyaudio (see use_pygame_audio below). This object uses Pygame for graphics. See bottom for other supported libraries.
## Arguments
- ```path``` - Path to video file. I tested a few popular video types, such as mkv, mp4, mov, avi, and 3gp, but theoretically anything FFMPEG can extract data from should work.
- ```chunk_size``` - How much audio is extracted at a time, in seconds. Increasing this value can mean less total extracts, but slower extracts.
- ```max_threads``` - Maximum number of chunks that can be extracted at any given time. Increasing this value can speed up extract at the expense of cpu usage.
- ```max_chunks``` - Maximum number of chunks allowed to be extracted and reserved. Increasing this value can help with buffering, but will use more memory.
- ```subs``` - Pass a Subtitle class here for the video to display subtitles.
- ```post_process``` - Post processing function that is applied whenever a frame is rendered. This is PostProcessing.none by default, which means no alterations are taking place.
- ```interp``` - Interpolation technique used when resizing frames. In general, the three main ones are cv2.INTER_LINEAR, which is balanced, cv2.INTER_CUBIC, which is slower but produces better results, and cv2.INTER_AREA, which is better for downscaling.
- ```use_pygame_audio``` - Specifies whether to use Pyaudio or Pygame to play audio.
## Attributes
- ```path``` - Same as given argument.
- ```name``` - Name of file without the directory and extension.
- ```ext``` - Type of video (mp4, mkv, mov, etc).
- ```frame``` - Current frame index. Starts from 0.
- ```frame_rate``` - How many frames are in one second.
- ```frame_count``` - How many total frames there are.
- ```frame_delay``` - Time between frames in order to maintain frame rate (in fractions of a second).
- ```duration``` - Length of video in seconds.
- ```original_size```
- ```current_size```
- ```aspect_ratio``` - Width divided by height.
- ```chunk_size``` - Same as given argument.
- ```max_chunks``` - Same as given argument.
- ```max_threads``` - Same as given argument.
- ```frame_data``` - Current video frame as a NumPy ndarray.
- ```frame_surf``` - Current video frame as a Pygame Surface.
- ```active``` - Whether the video is currently playing. This is unaffected by pausing and resuming.
- ```buffering``` - Whether the video is waiting for audio to extract.
- ```paused```
- ```muted```
- ```speed``` - Float from 0.5 to 10.0 that multiplies the playback speed.
- ```subs``` - Same as given argument.
- ```post_func``` - Same as given argument.
- ```interp``` - Same as given argument.
- ```use_pygame_audio``` - Same as given argument.
## Methods
- ```play()```
- ```stop()```
- ```resize(size)```
- ```change_resolution(height)``` - Given a height, the video will scale its width while maintaining aspect ratio.
- ```close()``` - Releases resources. Always recommended to call when done.
- ```restart() ```
- ```set_speed(speed)``` - Accepts a float from 0.5 (half speed) to 10.0 (ten times speed)
- ```get_speed()```
- ```set_volume(volume)``` - Adjusts the volume of the video, from 0.0 (min) to 1.0 (max).
- ```get_volume()```
- ```get_paused()```
- ```toggle_pause()``` - Pauses if the video is playing, and resumes if the video is paused.
- ```pause()```
- ```resume()```
- ```toggle_mute()```
- ```mute()```
- ```unmute()```
- ```get_pos()``` - Returns the current position in seconds.
- ```seek(time, relative=True)``` - Changes the current position in the video. If relative is true, the given time will be added or subtracted to the current time. Otherwise, the current position will be set to the given time exactly. Time must be given in seconds, and seeking will be accurate to one tenth of a second.
- ```draw(surf, pos, force_draw=True)``` - Draws the current video frame onto the given surface, at the given position. If force_draw is true, a surface will be drawn every time this is called. Otherwise, only new frames will be drawn. This reduces cpu usage, but will cause flickering if anything is drawn under or above the video. This method also returns whether a frame was drawn.
- ```preview()``` - Opens a window and plays the video. This method will hang until the video closes. Videos are played at 60 fps with force_draw disabled.
# VideoPlayer(video, rect, interactable=True, loop=False, preview_thumbnails=0)
VideoPlayers are GUI containers for videos. This seeks to mimic standard video players, so clicking it will play/pause playback, and the GUI will only show when the mouse is hovering over it. Only supported for Pygame.
## Arguments
- ```video``` - Video object to play.
- ```rect``` - An x, y, width, and height of the VideoPlayer. The topleft corner will be the x, y coordinate.
- ```interactable``` - Enables the GUI.
- ```loop``` - Whether the contained video will restart after it finishes. If the queue is not empty, the entire queue will loop, not just the current video.
- ```preview_thumbnails``` - Number of preview thumbnails loaded and saved in memory. When seeking, a preview window will show the closest loaded frame. The higher this number is, the more frames are loaded, increasing the preview accuracy, but also increasing initial load time and memory usage. Because of this, this value is defaulted to 0, which turns seek previewing off.
## Attributes
- ```video``` - Same as given argument.
- ```frame_rect``` - Same as given argument.
- ```vid_rect``` - This is the video fitted into the frame_rect while maintaining aspect ratio. Black bars will appear in any unused space.
- ```interactable``` - Same as given argument.
- ```loop``` - Same as given argument.
- ```queue_``` - Videos to play after the current one finishes.
- ```preview_thumbnails``` - Same as given argument.
## Methods
- ```zoom_to_fill()``` - Zooms in the video so that the entire frame_rect is filled in, while maintaining aspect ratio.
- ```zoom_out()``` - Reverts zoom_to_fill()
- ```queue(input)``` - Accepts a path to a video or a Video object and adds it to the queue. Passing a path will not load the video until it becomes the active video. Passing a Video object will cause it to silently load its first audio chunk, so changing videos will be as seamless as possible.
- ```get_queue()```
- ```resize(size)```
- ```move(pos, relative)``` - Moves the VideoPlayer. If relative is true, the given coordinates will be added onto the current coordinates. Otherwise, the current coordinates will be set to the given coordinates.
- ```update(events, show_ui=None)``` - Allows the VideoPlayer to make calculations. It must be given the returns of pygame.event.get(). The GUI automatically shows up when your mouse hovers over the video player, so show_ui can be used to override that. This method also returns show_ui.
- ```draw(surface)``` - Draws the VideoPlayer onto the given Surface.
- ```close()``` - Releases resources. Always recommended to call when done.
- ```skip()``` - Moves onto the next video in the queue.
- ```get_video()``` - Returns currently playing video.
# Subtitles(path, colour="white", highlight=(0, 0, 0, 128), font=pygame.font.SysFont("arial", 30), encoding="utf-8-sig")
Object used for handling subtitles. Only supported for Pygame.
## Arguments
- ```path``` - Path to subtitle file. This can be any file pysubs2 can read, including .srt, .ass, .vtt, and others.
- ```colour``` - Colour of text.
- ```highlight``` - Background colour of text. Accepts RGBA, so it can be made completely transparent.
- ```font``` - Pygame Font or SysFont object used to render Surfaces. This includes the size of the text.
- ```encoding``` - Encoding used to open the srt file.
- ```offset``` - The higher this number is, the close the subtitle is to the top of the screen.
## Attributes
- ```path``` - Same as given argument.
- ```encoding``` - Same as given argument.
- ```start``` - Starting timestamp of current subtitle.
- ```end``` - Ending timestamp of current subtitle.
- ```text``` - Current subtitle text.
- ```surf``` - Current text in a Pygame Surface.
- ```colour``` - Same as given argument.
- ```highlight``` - Same as given argument.
- ```font``` - Same as given argument.
- ```offset``` - Same as given argument.
## Methods
- ```set_font(font)```
- ```get_font()```
# Webcam(post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, fps=30)
Object used for displaying a webcam feed. Only supported for Pygame.
## Arguments
- ```post_process``` - Post processing function that is applied whenever a frame is rendered. This is PostProcessing.none by default, which means no alterations are taking place.
- ```interp``` - Interpolation technique used when resizing frames. In general, the three main ones are cv2.INTER_LINEAR, which is balanced, cv2.INTER_CUBIC, which is slower but produces better results, and cv2.INTER_AREA, which is better for downscaling.
- ```fps``` - Maximum number of frames captured from the webcam per second.
## Attributes
- ```post_process``` - Same as given argument.
- ```interp``` - Same as given argument.
- ```fps``` - Same as given argument.
- ```original_size```
- ```current_size```
- ```aspect_ratio``` - Width divided by height.
- ```active``` - Whether the webcam is currently playing.
- ```frame_data``` - Current video frame as a NumPy ndarray.
- ```frame_surf``` - Current video frame as a Pygame Surface.
## Methods
- ```play()```
- ```stop()```
- ```resize(size)```
- ```change_resolution(height)``` - Given a height, the video will scale its width while maintaining aspect ratio.
- ```close()``` - Releases resources. Always recommended to call when done.
- ```get_pos()``` - Returns how long the webcam has been active. Is not reset if webcam is stopped.
- ```draw(surf, pos, force_draw=True)``` - Draws the current video frame onto the given surface, at the given position. If force_draw is true, a surface will be drawn every time this is called. Otherwise, only new frames will be drawn. This reduces cpu usage, but will cause flickering if anything is drawn under or above the video. This method also returns whether a frame was drawn.
- ```preview()``` - Opens a window and plays the webcam. This method will hang until the window is closed. Videos are played at whatever fps the webcam object is set to.
# PostProcessing
Used to apply various filters to video playback. Mostly for fun. Works across all graphics libraries.
- ```none``` - Default. Nothing happens.
- ```blur``` - Slightly blurs frames.
- ```sharpen``` - An okay-looking sharpen. Looks pretty bad for small resolutions.
- ```greyscale``` - Removes colour from frame.
- ```noise``` - Adds a static-like filter. Very intensive.
- ```letterbox``` - Adds black bars above and below the frame to look more cinematic.
- ```cel_shading``` - Thickens borders for a comic book style filter.
# Supported Graphics Libraries
- Pygame (```Video```) <- default and best supported
- Tkinter (```VideoTkinter```)
- Pyglet (```VideoPyglet```)
- PyQT6 (```VideoPyQT```)
To use other libraries instead of Pygame, use their respective video object. Each preview method will use their respective graphics API to create a window and draw frames. See the examples folder for details. Note that Subtitles, Webcam, and VideoPlayer only work with Pygame installed.
# Get Version
```
print(pyvidplayer2.get_version_info())
```
Returns a dictionary with the version of pyvidplayer2, FFMPEG, and Pygame.

View File

@@ -0,0 +1,13 @@
'''
This shows off each graphics api and their respective preview methods
'''
from pyvidplayer2 import Video, VideoTkinter, VideoPyglet, VideoPyQT
PATH = r"resources\trailer1.mp4"
Video(PATH).preview()
VideoTkinter(PATH).preview()
VideoPyglet(PATH).preview()
VideoPyQT(PATH).preview()

View File

@@ -0,0 +1,7 @@
'''
This is an example of the cel shading post process you can apply to your videos
'''
from pyvidplayer2 import Video, PostProcessing
Video(r"resources\medic.mov", post_process=PostProcessing.cel_shading).preview()

View File

@@ -0,0 +1,13 @@
'''
This example shows how you can merge and create new post processing functions
'''
from pyvidplayer2 import Video, PostProcessing
# applies a letterbox, cel shading, and greyscale to video
def custom_process(data):
return PostProcessing.letterbox(PostProcessing.cel_shading(PostProcessing.greyscale(data)))
Video(r"resources\birds.avi", post_process=custom_process).preview()

View File

@@ -0,0 +1,11 @@
'''
This is an example of custom subtitle fonts
'''
from pyvidplayer2 import Subtitles, Video
from pygame.font import Font
subtitles = Subtitles(r"resources\subs2.srt", font=Font(r"resources\font.ttf", 60), highlight=(255, 0, 0, 128), offset=500)
Video(r"resources\trailer2.mp4", subs=subtitles).preview()

View File

@@ -0,0 +1,39 @@
'''
This is an example of a VideoCollection, which allows you to treat a large
amount of ParallelVideos as one
'''
import pygame
from pyvidplayer2 import Video, VideoPlayer
win = pygame.display.set_mode((1066, 744))
pygame.display.set_caption("video collection demo")
videos = [VideoPlayer(Video(r"resources\billiejean.mp4"), (0, 0, 426, 240), interactable=False),
VideoPlayer(Video(r"resources\trailer1.mp4"), (426, 0, 256, 144), interactable=False),
VideoPlayer(Video(r"resources\medic.mov"), (682, 0, 256, 144), interactable=False),
VideoPlayer(Video(r"resources\trailer2.mp4"), (426, 144, 640, 360), interactable=False),
VideoPlayer(Video(r"resources\clip.mp4"), (0, 240, 256, 144), interactable=False),
VideoPlayer(Video(r"resources\birds.avi"), (0, 384, 426, 240), interactable=False),
VideoPlayer(Video(r"resources\ocean.mkv"), (426, 504, 426, 240), interactable=False)]
while True:
key = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
[video.close() for video in videos]
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN:
key = pygame.key.name(event.key)
pygame.time.wait(16)
win.fill("white")
[video.update() for video in videos]
[video.draw(win) for video in videos]
pygame.display.update()

View File

@@ -0,0 +1,32 @@
'''
This is an example of two videos playing simultaneously
'''
import pygame
from pyvidplayer2 import Video
win = pygame.display.set_mode((960, 360))
pygame.display.set_caption("parallel playing demo")
vid1 = Video(r"resources\trailer1.mp4")
vid1.resize((480, 360))
vid2 = Video(r"resources\trailer2.mp4")
vid2.resize((480, 360))
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
vid1.close()
vid2.close()
pygame.quit()
exit()
pygame.time.wait(16)
vid1.draw(win, (0, 0))
vid2.draw(win, (480, 0))
pygame.display.update()

View File

@@ -0,0 +1,32 @@
'''
This example shows how every video can play subtitles
'''
import pygame
from pyvidplayer2 import Video, Subtitles
win = pygame.display.set_mode((960, 360))
pygame.display.set_caption("parallel subtitles demo")
vid1 = Video(r"resources\trailer1.mp4", subs=Subtitles(r"resources\subs1.srt"))
vid1.resize((480, 360))
vid2 = Video(r"resources\trailer2.mp4", subs=Subtitles(r"resources\subs2.srt"))
vid2.resize((480, 360))
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
vid1.close()
vid2.close()
pygame.quit()
exit()
pygame.time.wait(16)
vid1.draw(win, (0, 0))
vid2.draw(win, (480, 0))
pygame.display.update()

View File

@@ -0,0 +1,59 @@
'''
A quick example showing how pyvidplayer2 can be used in more complicated applications
This is a Picture-in-Picture app
'''
import pygame
from win32gui import SetWindowPos, GetCursorPos, GetWindowRect, GetForegroundWindow, SetForegroundWindow
from win32api import GetSystemMetrics
from win32con import SWP_NOSIZE, HWND_TOPMOST
from win32com.client import Dispatch
from pyvidplayer2 import VideoPlayer, Video
from cv2 import INTER_AREA
SIZE = (426, 240)
FILE = r"resources\billiejean.mp4"
win = pygame.display.set_mode(SIZE, pygame.NOFRAME)
# creates the video player
vid = VideoPlayer(Video(FILE, interp=INTER_AREA), (0, 0, *SIZE))
# moves the window to the bottom right corner and pins it above other windows
hwnd = pygame.display.get_wm_info()["window"]
SetWindowPos(hwnd, HWND_TOPMOST, GetSystemMetrics(0) - SIZE[0], GetSystemMetrics(1) - SIZE[1] - 48, 0, 0, SWP_NOSIZE)
clock = pygame.time.Clock()
shell = Dispatch("WScript.Shell")
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
vid.close()
pygame.quit()
quit()
clock.tick(60)
# allows the ui to be seamlessly interacted with
touching = pygame.Rect(GetWindowRect(hwnd)).collidepoint(GetCursorPos())
if touching and GetForegroundWindow() != hwnd:
# weird behaviour with SetForegroundWindow that requires the alt key to be pressed before it's called
shell.SendKeys("%")
SetForegroundWindow(hwnd)
# handles video playback
vid.update(events, show_ui=touching)
vid.draw(win)
pygame.display.update()

View File

@@ -0,0 +1,10 @@
'''
This example shows how you can control the playback speed of videos
'''
from pyvidplayer2 import Video
v = Video(r"resources\trailer1.mp4")
v.set_speed(2) # twice as fast
v.preview()

View File

@@ -0,0 +1,42 @@
'''
This example gives a side by side comparison between a few available post process effects
'''
import pygame
from pyvidplayer2 import Video, PostProcessing
PATH = r"resources\ocean.mkv"
win = pygame.display.set_mode((960, 240))
pygame.display.set_caption("post processing demo")
# using a video collection to play videos in parallel for a side to side comparison
videos = [Video(PATH, post_process=PostProcessing.sharpen),
Video(PATH),
Video(PATH, post_process=PostProcessing.blur)]
font = pygame.font.SysFont("arial", 30)
surfs = [font.render("Sharpen", True, "white"), font.render("Normal", True, "white"), font.render("Blur", True, "white")]
while True:
key = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
[video.close() for video in videos]
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN:
key = pygame.key.name(event.key)
pygame.time.wait(16)
for i, surf in enumerate(surfs):
x = 320 * i
videos[i].draw(win, (x, 0))
pygame.draw.rect(win, "black", (x, 0, *surf.get_size()))
win.blit(surf, (x, 0))
pygame.display.update()

View File

@@ -0,0 +1,8 @@
'''
This is the quickest and simplest way to play videos
'''
from pyvidplayer2 import Video
Video(r"resources\trailer1.mp4").preview()

View File

@@ -0,0 +1,22 @@
'''
This is a quick example of integrating a video into a pyglet project
Double buffering is turned off to benefit from turning off force draw on the video
'''
import pyglet
from pyvidplayer2 import VideoPyglet
video = VideoPyglet(r"resources\trailer1.mp4")
def update(dt):
video.draw((0, 0), force_draw=False)
if not video.active:
win.close()
win = pyglet.window.Window(width=video.current_size[0], height=video.current_size[1], config=pyglet.gl.Config(double_buffer=False), caption=f"pyglet support demo")
pyglet.clock.schedule_interval(update, 1/60.0)
pyglet.app.run()
video.close()

View File

@@ -0,0 +1,33 @@
'''
This is a quick example of integrating a video into a pyqt6 project
'''
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt6.QtCore import QTimer
from pyvidplayer2 import VideoPyQT
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.canvas = QWidget(self)
self.setCentralWidget(self.canvas)
self.timer = QTimer(self)
self.timer.timeout.connect(self.update)
self.timer.start(16)
def paintEvent(self, _):
video.draw(self, (0, 0))
video = VideoPyQT(r"resources\trailer1.mp4")
app = QApplication([])
win = Window()
win.setWindowTitle(f"pyqt6 support demo")
win.setFixedSize(*video.current_size)
win.show()
app.exec()
video.close()

View File

@@ -0,0 +1,32 @@
'''
This example shows how videos can be queued and skipped through with the VideoPlayer object
'''
import pygame
from pyvidplayer2 import VideoPlayer, Video
win = pygame.display.set_mode((1280, 720))
vid = VideoPlayer(Video(r"resources\clip.mp4"), (0, 0, 1280, 720), loop=True)
vid.queue(Video(r"resources\ocean.mkv"))
vid.queue(Video(r"resources\birds.avi"))
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
vid.close()
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
vid.skip()
pygame.time.wait(16)
vid.update(events)
vid.draw(win)
pygame.display.update()

View File

@@ -0,0 +1,8 @@
'''
This is an example showing how to add subtitles to a video
'''
from pyvidplayer2 import Subtitles, Video
Video(r"resources\trailer2.mp4", subs=Subtitles(r"resources\subs2.srt")).preview()

View File

@@ -0,0 +1,27 @@
'''
This is a quick example of integrating a video into a tkinter project
'''
import tkinter
from pyvidplayer2 import VideoTkinter
video = VideoTkinter(r"resources\trailer1.mp4")
def update():
video.draw(canvas, (video.current_size[0] / 2, video.current_size[1] / 2), force_draw=False)
if video.active:
root.after(16, update) # for around 60 fps
else:
root.destroy()
root = tkinter.Tk()
root.title(f"tkinter support demo")
canvas = tkinter.Canvas(root, width=video.current_size[0], height=video.current_size[1], highlightthickness=0)
canvas.pack()
update()
root.mainloop()
video.close()

View File

@@ -0,0 +1,46 @@
'''
This is the same example from the original pyvidplayer
The video class still does everything it did, but with many more features
'''
import pygame
from pyvidplayer2 import Video
pygame.init()
win = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
#provide video class with the path to your video
vid = Video(r"resources\medic.mov")
while True:
key = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
vid.close()
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN:
key = pygame.key.name(event.key)
#your program frame rate does not affect video playback
clock.tick(60)
if key == "r":
vid.restart() #rewind video to beginning
elif key == "p":
vid.toggle_pause() #pause/plays video
elif key == "right":
vid.seek(15) #skip 15 seconds in video
elif key == "left":
vid.seek(-15) #rewind 15 seconds in video
elif key == "up":
vid.set_volume(1.0) #max volume
elif key == "down":
vid.set_volume(0.0) #min volume
#draws the video to the given surface, at the given position
vid.draw(win, (0, 0), force_draw=False)
pygame.display.update()

View File

@@ -0,0 +1,30 @@
'''
This is an example of the built in GUI for videos
'''
import pygame
from pyvidplayer2 import VideoPlayer, Video
win = pygame.display.set_mode((1124, 868))
pygame.display.set_caption("video player demo")
vid = VideoPlayer(Video(r"resources\ocean.mkv"), (50, 50, 1024, 768), preview_thumbnails=11)
while True:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
vid.close()
pygame.quit()
exit()
pygame.time.wait(16)
win.fill("white")
vid.update(events)
vid.draw(win)
pygame.display.update()

View File

@@ -0,0 +1,24 @@
'''
Webcam example
'''
import pygame
from pyvidplayer2 import Webcam
webcam = Webcam()
win = pygame.display.set_mode(webcam.current_size)
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
webcam.close()
pygame.quit()
exit()
clock.tick(60)
webcam.draw(win, (0, 0), force_draw=False)
pygame.display.update()

View File

@@ -0,0 +1,49 @@
import subprocess
from .post_processing import PostProcessing
from .video_tkinter import VideoTkinter
try:
import PyQt6
except ImportError:
pass
else:
from .video_pyqt import VideoPyQT
try:
import pygame
except ImportError:
pass
else:
pygame.init()
from .video_pygame import VideoPygame as Video
from .subtitles import Subtitles
from .video_player import VideoPlayer
from .webcam import Webcam
try:
import pyglet
except ImportError:
pass
else:
from .video_pyglet import VideoPyglet
_VERSION = "0.9.11"
def get_version_info() -> dict:
try:
pygame_ver = pygame.version.ver
except NameError:
pygame_ver = "not installed"
try:
ffmpeg_ver = subprocess.run(["ffmpeg", "-version"], capture_output=True, universal_newlines=True).stdout.split(" ")[2]
except FileNotFoundError:
ffmpeg_ver = "not installed"
return {"pyvidplayer2": _VERSION,
"ffmpeg": ffmpeg_ver,
"pygame": pygame_ver}

View File

@@ -0,0 +1,2 @@
class Pyvidplayer2Error(Exception):
pass

View File

@@ -0,0 +1,50 @@
import pygame
from io import BytesIO
class MixerHandler:
def __init__(self) -> None:
self.muted = False
self.volume = 1
def get_busy(self):
return pygame.mixer.music.get_busy()
def load(self, bytes):
pygame.mixer.music.load(BytesIO(bytes))
def unload(self):
self.stop()
pygame.mixer.music.unload()
def play(self):
pygame.mixer.music.play()
def set_volume(self, vol):
self.volume = vol
pygame.mixer.music.set_volume(min(1.0, max(0.0, vol)))
def get_volume(self):
return self.volume
def get_pos(self):
return max(0, pygame.mixer.music.get_pos()) / 1000
def stop(self):
pygame.mixer.music.stop()
def pause(self):
pygame.mixer.music.pause()
def unpause(self):
# unpausing the mixer when nothing has been loaded causes weird behaviour
if pygame.mixer.music.get_pos() != -1:
pygame.mixer.music.unpause()
def mute(self):
self.muted = True
pygame.mixer.music.set_volume(0)
def unmute(self):
self.muted = False
self.set_volume(self.volume)

View File

@@ -0,0 +1,33 @@
import cv2
import numpy
class PostProcessing:
def none(data: numpy.ndarray) -> numpy.ndarray:
return data
def blur(data: numpy.ndarray) -> numpy.ndarray:
return cv2.blur(data, (5, 5))
def sharpen(data: numpy.ndarray) -> numpy.ndarray:
return cv2.filter2D(data, -1, numpy.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]))
def greyscale(data: numpy.ndarray) -> numpy.ndarray:
return numpy.stack((cv2.cvtColor(data, cv2.COLOR_BGR2GRAY),) * 3, axis=-1)
def noise(data: numpy.ndarray) -> numpy.ndarray:
noise = numpy.zeros(data.shape, dtype=numpy.uint8)
cv2.randn(noise, (0,) * 3, (20,) * 3)
return data + noise
def letterbox(data: numpy.ndarray) -> numpy.ndarray:
background = numpy.zeros((*data.shape[:2], 3), dtype=numpy.uint8)
x1, y1 = 0, int(data.shape[0] * 0.1) #topleft crop
x2, y2 = data.shape[1], int(data.shape[0] * 0.9) #bottomright crop
data = data[y1:y2, x1:x2] # crops image
background[y1:y1 + data.shape[0], x1:x1 + data.shape[1]] = data # draws image onto background
return background
def cel_shading(data: numpy.ndarray) -> numpy.ndarray:
return cv2.subtract(data, cv2.blur(cv2.merge((cv2.Canny(data, 150, 200),) * 3), (2, 2)))

View File

@@ -0,0 +1,125 @@
import pyaudio
import wave
import math
import time
import numpy
from threading import Thread
from io import BytesIO
class PyaudioHandler:
def __init__(self) -> None:
self.stream = None
self.wave = None
self.thread = None
self.stop_thread = False
self.position = 0
self.loaded = False
self.paused = False
self.active = False
self.volume = 1.0
self.muted = False
self.p = pyaudio.PyAudio()
self.stream = None
def get_busy(self):
return self.active
def load(self, bytes):
self.unload()
try:
self.wave = wave.open(BytesIO(bytes), "rb")
except EOFError:
raise EOFError("Audio is empty. This may mean the file is corrupted.")
if self.stream is None:
self.stream = self.p.open(
format=self.p.get_format_from_width(self.wave.getsampwidth()),
channels=self.wave.getnchannels(),
rate=self.wave.getframerate(),
output=True)
self.loaded = True
def close(self):
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
def unload(self):
if self.loaded:
self.stop()
self.wave.close()
self.wave = None
self.thread = None
self.loaded = False
def play(self):
self.stop_thread = False
self.position = 0
self.active = True
self.wave.rewind()
self.thread = Thread(target=self._threaded_play)
self.thread.start()
def _threaded_play(self):
chunk = 2048
data = self.wave.readframes(chunk)
while data != b'' and not self.stop_thread:
if self.paused:
time.sleep(0.01)
else:
audio = numpy.frombuffer(data, dtype=numpy.int16)
if self.volume == 0.0 or self.muted:
audio = numpy.zeros_like(audio)
else:
db = 20 * math.log10(self.volume)
audio = (audio * 10**(db/20)).astype(numpy.int16)
self.stream.write(audio.tobytes())
data = self.wave.readframes(chunk)
self.position += chunk / self.wave.getframerate()
self.active = False
def set_volume(self, vol):
self.volume = min(1.0, max(0.0, vol))
def get_volume(self):
return self.volume
def get_pos(self):
return self.position
def stop(self):
if self.loaded:
self.stop_thread = True
self.thread.join()
self.position = 0
def pause(self):
self.paused = True
def unpause(self):
self.paused = False
def mute(self):
self.muted = True
def unmute(self):
self.muted = False

View File

@@ -0,0 +1,68 @@
import pygame
import pysubs2
class Subtitles:
def __init__(self, path: str, colour="white", highlight=(0, 0, 0, 128), font=pygame.font.SysFont("arial", 30), encoding="utf-8", offset=50) -> None:
self.path = path
self.encoding = encoding
self._subs = iter(pysubs2.load(path, encoding=encoding))
self.start = 0
self.end = 0
self.text = ""
self.surf = pygame.Surface((0, 0))
self.offset = offset
self.colour = colour
self.highlight = highlight
self.font = font
def __str__(self) -> str:
return f"<Subtitles(path={self.path})>"
def _to_surf(self, text: str) -> pygame.Surface:
h = self.font.render(" ", True, "black").get_height()
lines = text.strip().split("\n")
surfs = [self.font.render(line, True, self.colour) for line in lines]
surface = pygame.Surface((max([s.get_width() for s in surfs]), len(surfs) * h), pygame.SRCALPHA)
surface.fill(self.highlight)
for i, surf in enumerate(surfs):
surface.blit(surf, (surface.get_width() / 2 - surf.get_width() / 2, i * h))
return surface
def _get_next(self) -> bool:
try:
s = next(self._subs)
except StopIteration:
self.start = 0
self.end = 0
self.text = ""
self.surf = pygame.Surface((0, 0))
return False
else:
self.start = s.start / 1000
self.end = s.end / 1000
self.text = s.plaintext
self.surf = self._to_surf(self.text)
return True
def _seek(self, time: float) -> None:
self._subs = iter(pysubs2.load(self.path, encoding=self.encoding))
while not (self.start <= time <= self.end):
if not self._get_next():
break
def _write_subs(self, surf: pygame.Surface) -> None:
surf.blit(self.surf, (surf.get_width() / 2 - self.surf.get_width() / 2, surf.get_height() - self.surf.get_height() - self.offset))
def set_font(self, font: pygame.font.SysFont) -> None:
self.font = font
def get_font(self) -> pygame.font.SysFont:
return self.font

View File

@@ -0,0 +1,299 @@
import cv2
import subprocess
import os
from typing import Tuple
from threading import Thread
from .pyaudio_handler import PyaudioHandler
try:
import pygame
except ImportError:
pass
else:
from .mixer_handler import MixerHandler
class Video:
def __init__(self, path: str, chunk_size, max_threads, max_chunks, subs, post_process, interp, use_pygame_audio) -> None:
self.path = path
self.name, self.ext = os.path.splitext(os.path.basename(self.path))
self._vid = cv2.VideoCapture(self.path)
if not self._vid.isOpened():
raise FileNotFoundError(f'Could not find "{self.path}"')
# file information
self.frame_count = int(self._vid.get(cv2.CAP_PROP_FRAME_COUNT))
self.frame_rate = self._vid.get(cv2.CAP_PROP_FPS)
self.frame_delay = 1 / self.frame_rate
self.duration = self.frame_count / self.frame_rate
self.original_size = (int(self._vid.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._vid.get(cv2.CAP_PROP_FRAME_HEIGHT)))
self.current_size = self.original_size
self.aspect_ratio = self.original_size[0] / self.original_size[1]
self.chunk_size = chunk_size
self.max_chunks = max_chunks
self.max_threads = max_threads
self._chunks = []
self._threads = []
self._starting_time = 0
self._chunks_claimed = 0
self._chunks_played = 0
self._stop_loading = False
self.frame = 0
self.frame_data = None
self.frame_surf = None
self.active = False
self.buffering = False
self.paused = False
self.muted = False
self.subs = subs
self.post_func = post_process
self.interp = interp
self.use_pygame_audio = use_pygame_audio
if use_pygame_audio:
try:
self._audio = MixerHandler()
except NameError:
raise ModuleNotFoundError("Unable to use Pygame audio because Pygame is not installed.")
else:
self._audio = PyaudioHandler()
self.speed = 1
self._missing_ffmpeg = False # for throwing errors
self.play()
def _chunks_len(self) -> int:
i = 0
for c in self._chunks:
if c is not None:
i += 1
return i
def _convert_seconds(self, seconds: float) -> str:
h = int(seconds // 3600)
seconds = seconds % 3600
m = int(seconds // 60)
s = int(seconds % 60)
d = round(seconds % 1, 1)
return f"{h}:{m}:{s}.{int(d * 10)}"
def _threaded_load(self, index) -> None:
i = index # assigned to variable so another thread does not change it
self._chunks.append(None)
s = self._convert_seconds((self._starting_time + (self._chunks_claimed - 1) * self.chunk_size) * (1 / self.speed))
command = [
"ffmpeg",
"-i",
self.path,
"-ss",
str(s),
"-t",
str(self._convert_seconds(self.chunk_size)),
"-vn",
"-f",
"wav",
"-loglevel",
"quiet",
"-"
]
filters = []
if self.speed != 1:
filters += ["-filter:a", f"atempo={self.speed}"]
command = command[:7] + filters + command[7:]
try:
p = subprocess.run(command, capture_output=True)
except FileNotFoundError:
self._missing_ffmpeg = True
self._chunks[i - self._chunks_played - 1] = p.stdout
def _update_threads(self) -> None:
for t in self._threads:
if not t.is_alive():
self._threads.remove(t)
self._stop_loading = self._starting_time + self._chunks_claimed * self.chunk_size >= self.duration
if not self._stop_loading and len(self._threads) < self.max_threads and self._chunks_len() + len(self._threads) < self.max_chunks:
self._chunks_claimed += 1
self._threads.append(Thread(target=self._threaded_load, args=(self._chunks_claimed,)))
self._threads[-1].start()
def _write_subs(self) -> None:
p = self.get_pos()
if p >= self.subs.start:
if p > self.subs.end:
if self.subs._get_next():
self._write_subs()
else:
self.subs._write_subs(self.frame_surf)
def _update(self) -> bool:
if self._missing_ffmpeg:
raise FileNotFoundError("Could not find FFMPEG. Make sure it's downloaded and accessible via $PATH.")
self._update_threads()
n = False
self.buffering = False
if self._audio.get_busy() or self.paused:
while self.get_pos() > self.frame * self.frame_delay:
has_frame, data = self._vid.read()
self.frame += 1
if has_frame:
if self.original_size != self.current_size:
data = cv2.resize(data, dsize=self.current_size, interpolation=self.interp)
data = self.post_func(data)
self.frame_data = data
self.frame_surf = self._create_frame(data)
if self.subs is not None:
self._write_subs()
n = True
else:
break
elif self.active:
if self._chunks and self._chunks[0] is not None:
self._chunks_played += 1
self._audio.load(self._chunks.pop(0))
self._audio.play()
elif self._stop_loading and self._chunks_played == self._chunks_claimed:
self.stop()
else:
self.buffering = True
return n
def mute(self) -> None:
self.muted = True
self._audio.mute()
def unmute(self) -> None:
self.muted = False
self._audio.unmute()
def set_speed(self, speed: float) -> None:
speed = max(0.5, min(10, speed))
if speed != self.speed:
self.speed = speed
self.seek(0) # must reload audio chunks
def get_speed(self) -> float:
return self.speed
def play(self) -> None:
self.active = True
def stop(self) -> None:
self.restart()
self.active = False
self.frame_data = None
self.frame_surf = None
self.paused = False
def resize(self, size: Tuple[int, int]) -> None:
self.current_size = size
def change_resolution(self, height: int) -> None:
self.current_size = (int(height * self.aspect_ratio), height)
def close(self) -> None:
self.stop()
self._vid.release()
self._audio.unload()
for t in self._threads:
t.join()
if not self.use_pygame_audio:
self._audio.close()
def restart(self) -> None:
self.seek(0, relative=False)
self.play()
def set_volume(self, vol: float) -> None:
self._audio.set_volume(vol)
def get_volume(self) -> float:
return self._audio.get_volume()
def get_paused(self) -> bool:
# here because the original pyvidplayer had get_paused
return self.paused
def toggle_pause(self) -> None:
self.resume() if self.paused else self.pause()
def toggle_mute(self) -> None:
self.unmute() if self.muted else self.mute()
def pause(self) -> None:
if self.active:
self.paused = True
self._audio.pause()
def resume(self) -> None:
if self.active:
self.paused = False
self._audio.unpause()
def get_pos(self) -> float:
return self._starting_time + max(0, self._chunks_played - 1) * self.chunk_size + self._audio.get_pos() * self.speed
def seek(self, time: float, relative=True) -> None:
# seeking accurate to 1 tenth of a second
self._starting_time = (self.get_pos() + time) if relative else time
self._starting_time = round(min(max(0, self._starting_time), self.duration), 1)
for t in self._threads:
t.join()
self._chunks = []
self._threads = []
self._chunks_claimed = 0
self._chunks_played = 0
self._audio.unload()
self._vid.set(cv2.CAP_PROP_POS_FRAMES, self._starting_time * self.frame_rate)
self.frame = int(self._vid.get(cv2.CAP_PROP_POS_FRAMES))
if self.subs is not None:
self.subs._seek(self._starting_time)
def draw(self, surf, pos: Tuple[int, int], force_draw=True) -> bool:
if (self._update() or force_draw) and self.frame_surf is not None:
self._render_frame(surf, pos)
return True
return False
def _create_frame(self):
pass
def _render_frame(self):
pass
def preview(self):
pass

View File

@@ -0,0 +1,259 @@
import pygame
import cv2
import math
from typing import Tuple, List
from . import Video
class VideoPlayer:
def __init__(self, video: Video, rect: Tuple[int, int, int, int], interactable=True, loop=False, preview_thumbnails=0) -> None:
self.video = video
self.frame_rect = pygame.Rect(rect)
self.interactable = interactable
self.loop = loop
self.preview_thumbnails = min(max(preview_thumbnails, 0), self.video.frame_count)
self._show_intervals = self.preview_thumbnails != 0
self.vid_rect = pygame.Rect(0, 0, 0, 0)
self._progress_back = pygame.Rect(0, 0, 0, 0)
self._progress_bar = pygame.Rect(0, 0, 0, 0)
self._smooth_bar = 0 # used for making the progress bar look smooth when seeking
self._font = pygame.font.SysFont("arial", 0)
self._buffer_rect = pygame.Rect(0, 0, 0, 0)
self._buffer_angle = 0
self._transform(self.frame_rect)
self._show_ui = False
self.queue_ = []
self._clock = pygame.time.Clock()
self._seek_pos = 0
self._seek_time = 0
self._show_seek = False
self._fade_timer = 0
if self._show_intervals:
self._interval = self.video.duration / self.preview_thumbnails
self._interval_frames = []
self._get_interval_frames()
def __str__(self) -> str:
return f"<VideoPlayer(path={self.path})>"
def _close_queue(self):
for video in self.queue_:
try:
video.close()
except AttributeError:
pass
def _get_interval_frames(self):
size = (int(70 * self.video.aspect_ratio), 70)
for i in range(self.preview_thumbnails):
self.video._vid.set(cv2.CAP_PROP_POS_FRAMES, int(i * self.video.frame_rate * self._interval))
self._interval_frames.append(pygame.image.frombuffer(cv2.resize(self.video._vid.read()[1], dsize=size, interpolation=cv2.INTER_AREA).tobytes(), size, "BGR"))
# add last readable frame
i = 1
while True:
self.video._vid.set(cv2.CAP_PROP_POS_FRAMES, self.video.frame_count - i)
try:
self._interval_frames.append(pygame.image.frombuffer(cv2.resize(self.video._vid.read()[1], dsize=size, interpolation=cv2.INTER_AREA).tobytes(), size, "BGR"))
except:
i += 1
else:
break
self.video._vid.set(cv2.CAP_PROP_POS_FRAMES, 0)
def _get_closest_frame(self, time):
i = math.floor(time // self._interval)
if (i + 1) * self._interval - time >= self._interval // 2:
return self._interval_frames[i]
else:
return self._interval_frames[i + 1]
def _best_fit(self, rect: pygame.Rect, r: float) -> pygame.Rect:
s = rect.size
r = self.video.aspect_ratio
w = s[0]
h = int(w / r)
y = int(s[1] /2 - h / 2)
x = 0
if h > s[1]:
h = s[1]
w = int(h * r)
x = int(s[0] / 2 - w / 2)
y = 0
return pygame.Rect(rect.x + x, rect.y + y, w, h)
def _transform(self, rect: pygame.Rect) -> None:
self.frame_rect = rect
self.vid_rect = self._best_fit(self.frame_rect, self.video.aspect_ratio)
self.video.resize(self.vid_rect.size)
self._progress_back = pygame.Rect(self.frame_rect.x + 10, self.frame_rect.bottom - 25, self.frame_rect.w - 20, 15)
self._progress_bar = self._progress_back.copy()
self._font = pygame.font.SysFont("arial", 10)
if self.video.frame_data is not None:
self.video.frame_surf = pygame.transform.smoothscale(self.video.frame_surf, self.vid_rect.size)
self._buffer_rect = pygame.Rect(0, 0, 200, 200)
self._buffer_rect.center = self.frame_rect.center
def _move_angle(self, pos: Tuple[int, int], angle: float, distance: int) -> Tuple[float, float]:
return pos[0] + math.cos(angle) * distance, pos[1] + math.sin(angle) * distance
def _convert_seconds(self, time: float) -> str:
return self.video._convert_seconds(time).split(".")[0]
def zoom_to_fill(self):
s = max(abs(self.frame_rect.w - self.vid_rect.w), abs(self.frame_rect.h - self.vid_rect.h))
self.vid_rect.inflate_ip(s, s)
self.video.resize(self.vid_rect.size)
self.vid_rect.center = self.frame_rect.center
def zoom_out(self):
self.vid_rect = self._best_fit(self.frame_rect, self.video.aspect_ratio)
self.video.resize(self.vid_rect.size)
def queue(self, input_: str | Video) -> None:
self.queue_.append(input_)
# update once to trigger audio loading
try:
input_.stop()
input_._update()
except AttributeError:
pass
def resize(self, size: Tuple[int, int]) -> None:
self.frame_rect.size = size
self._transform(self.frame_rect)
def move(self, pos: Tuple[int, int], relative=False) -> None:
if relative:
self.frame_rect.move_ip(*pos)
else:
self.frame_rect.topleft = pos
self._transform(self.frame_rect)
def update(self, events: List[pygame.event.Event] = None, show_ui=None) -> bool:
dt = self._clock.tick()
if not self.video.active:
if self.queue_:
if self.loop:
self.queue(self.video)
input_ = self.queue_.pop(0)
try:
self.video = Video(input_)
except TypeError:
self.video = input_
self.video.play()
self._transform(self.frame_rect)
elif self.loop:
self.video.restart()
if self.video._update() and self.video.current_size > self.frame_rect.size:
self.video.frame_surf = self.video.frame_surf.subsurface(self.frame_rect.x - self.vid_rect.x, self.frame_rect.y - self.vid_rect.y, *self.frame_rect.size)
if self.interactable:
mouse = pygame.mouse.get_pos()
click = False
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
click = True
self._show_ui = self.frame_rect.collidepoint(mouse) if show_ui is None else show_ui
if self._show_ui:
self._progress_bar.w = self._progress_back.w * (self.video.get_pos() / self.video.duration)
self._smooth_bar += (self._progress_bar.w - self._smooth_bar) / (dt * 0.25)
self._show_seek = self._progress_back.collidepoint(mouse)
if self._show_seek:
t = (self._progress_back.w - (self._progress_back.right - mouse[0])) * (self.video.duration / self._progress_back.w)
self._seek_pos = self._progress_back.w * (round(t, 1) / self.video.duration) + self._progress_back.x
self._seek_time = t
if click:
self.video.seek(t, relative=False)
self.video.play()
elif click:
self.video.toggle_pause()
self._buffer_angle += dt / 10
return self._show_ui
def draw(self, win: pygame.Surface) -> None:
pygame.draw.rect(win, "black", self.frame_rect)
if self.video.frame_surf is not None:
win.blit(self.video.frame_surf, self.frame_rect.topleft if self.video.current_size > self.frame_rect.size else self.vid_rect.topleft)
if self._show_ui:
pygame.draw.line(win, (50, 50, 50), (self._progress_back.x, self._progress_back.centery), (self._progress_back.right, self._progress_back.centery), 5)
if self._smooth_bar > 1:
pygame.draw.line(win, "white", (self._progress_bar.x, self._progress_bar.centery), (self._progress_bar.x + self._smooth_bar, self._progress_bar.centery), 5)
f = self._font.render(self.video.name, True, "white")
win.blit(f, (self.frame_rect.x + 10, self.frame_rect.y + 10))
f = self._font.render(self._convert_seconds(self.video.get_pos()), True, "white")
win.blit(f, (self.frame_rect.x + 10, self._progress_bar.top - f.get_height() - 10))
if self._show_seek:
pygame.draw.line(win, "white", (self._seek_pos, self._progress_back.top), (self._seek_pos, self._progress_back.bottom), 2)
f = self._font.render(self._convert_seconds(self._seek_time), True, "white")
win.blit(f, (self._seek_pos - f.get_width() // 2, self._progress_back.y - 10 - f.get_height()))
if self._show_intervals:
surf = self._get_closest_frame(self._seek_time)
x = self._seek_pos - surf.get_width() // 2
x = min(max(x, self.frame_rect.x), self.frame_rect.right - surf.get_width())
win.blit(surf, (x, self._progress_back.y - 80 - f.get_height()))
if self.interactable:
if self.video.buffering:
for i in range(6):
a = math.radians(self._buffer_angle + i * 60)
pygame.draw.line(win, "white", self._move_angle(self.frame_rect.center, a, 10), self._move_angle(self.frame_rect.center, a, 30))
elif self.video.paused:
pygame.draw.rect(win, "white", (self.frame_rect.centerx - 15, self.frame_rect.centery - 20, 10, 40))
pygame.draw.rect(win, "white", (self.frame_rect.centerx + 5, self.frame_rect.centery - 20, 10, 40))
def close(self) -> None:
self.video.close()
self._close_queue()
def skip(self) -> None:
self.video.stop() if self.loop else self.video.close()
def get_next(self) -> None | Video | str:
return self.queue_[0] if self.queue_ else None
def clear_queue(self) -> None:
self._close_queue()
self.queue_ = []
def get_video(self) -> Video:
return self.video
def get_queue(self) -> List:
return self.queue_

View File

@@ -0,0 +1,34 @@
import cv2
import pygame
import numpy
from .video import Video
from typing import Tuple
from .post_processing import PostProcessing
class VideoPygame(Video):
def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, subs=None, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None:
Video.__init__(self, path, chunk_size, max_threads, max_chunks, subs, post_process, interp, use_pygame_audio)
def __str__(self) -> str:
return f"<VideoPygame(path={self.path})>"
def _create_frame(self, data: numpy.ndarray) -> pygame.Surface:
return pygame.image.frombuffer(data.tobytes(), self.current_size, "BGR")
def _render_frame(self, surf: pygame.Surface, pos: Tuple[int, int]) -> None:
surf.blit(self.frame_surf, pos)
def preview(self) -> None:
win = pygame.display.set_mode(self.current_size)
pygame.display.set_caption(f"pygame - {self.name}")
self.play()
while self.active:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.stop()
pygame.time.wait(16)
self.draw(win, (0, 0), force_draw=False)
pygame.display.update()
pygame.display.quit()
self.close()

View File

@@ -0,0 +1,36 @@
import cv2
import pyglet
import numpy
from .video import Video
from typing import Tuple
from .post_processing import PostProcessing
class VideoPyglet(Video):
def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None:
Video.__init__(self, path, chunk_size, max_threads, max_chunks, None, post_process, interp, use_pygame_audio)
def __str__(self) -> str:
return f"<VideoPyglet(path={self.path})>"
def _create_frame(self, data: numpy.ndarray) -> pyglet.image.ImageData:
return pyglet.image.ImageData(*self.current_size, "BGR", cv2.flip(data, 0).tobytes())
def _render_frame(self, pos: Tuple[int, int]) -> None:
self.frame_surf.blit(*pos)
def draw(self, pos: Tuple[int, int], force_draw=True) -> bool:
if (self._update() or force_draw) and self.frame_surf is not None:
self._render_frame(pos) # (0, 0) pos draws the video bottomleft
return True
return False
def preview(self) -> None:
def update(dt):
self.draw((0, 0), force_draw=False)
if not self.active:
win.close()
win = pyglet.window.Window(width=self.current_size[0], height=self.current_size[1], config=pyglet.gl.Config(double_buffer=False), caption=f"pyglet - {self.name}")
pyglet.clock.schedule_interval(update, 1/60.0)
pyglet.app.run()
self.close()

View File

@@ -0,0 +1,41 @@
import cv2
import numpy
from .video import Video
from typing import Tuple
from PyQt6.QtGui import QImage, QPixmap, QPainter
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt6.QtCore import QTimer
from .post_processing import PostProcessing
class VideoPyQT(Video):
def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None:
Video.__init__(self, path, chunk_size, max_threads, max_chunks, None, post_process, interp, use_pygame_audio)
def __str__(self) -> str:
return f"<VideoPyQT(path={self.path})>"
def _create_frame(self, data: numpy.ndarray) -> QImage:
return QImage(data, data.shape[1], data.shape[0], data.strides[0], QImage.Format.Format_BGR888)
def _render_frame(self, win: QMainWindow, pos: Tuple[int, int]) -> None: #must be called in paintEvent
QPainter(win).drawPixmap(*pos, QPixmap.fromImage(self.frame_surf))
def preview(self) -> None:
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.canvas = QWidget(self)
self.setCentralWidget(self.canvas)
self.timer = QTimer(self)
self.timer.timeout.connect(self.update)
self.timer.start(16)
def paintEvent(self_, _):
self.draw(self_, (0, 0))
app = QApplication([])
win = Window()
win.setWindowTitle(f"pyqt6 - {self.name}")
win.setFixedSize(*self.current_size)
win.show()
app.exec()
self.close()

View File

@@ -0,0 +1,36 @@
import cv2
import numpy
import tkinter
from .video import Video
from typing import Tuple
from .post_processing import PostProcessing
class VideoTkinter(Video):
def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None:
Video.__init__(self, path, chunk_size, max_threads, max_chunks, None, post_process, interp, use_pygame_audio)
def __str__(self) -> str:
return f"<VideoTkinter(path={self.path})>"
def _create_frame(self, data: numpy.ndarray) -> tkinter.PhotoImage:
h, w = data.shape[:2]
return tkinter.PhotoImage(width=w, height=h, data=f"P6 {w} {h} 255 ".encode() + cv2.cvtColor(data, cv2.COLOR_BGR2RGB).tobytes(), format='PPM')
def _render_frame(self, canvas: tkinter.Canvas, pos: Tuple[int, int]) -> None:
canvas.create_image(*pos, image=self.frame_surf)
def preview(self) -> None:
def update():
self.draw(canvas, (self.current_size[0] / 2, self.current_size[1] / 2), force_draw=False)
if self.active:
root.after(16, update) # for around 60 fps
else:
root.destroy()
root = tkinter.Tk()
root.title(f"tkinter - {self.name}")
canvas = tkinter.Canvas(root, width=self.current_size[0], height=self.current_size[1], highlightthickness=0)
canvas.pack()
update()
root.mainloop()
self.close()

View File

@@ -0,0 +1,107 @@
import cv2
import pygame
import time
import numpy
from .post_processing import PostProcessing
from .error import Pyvidplayer2Error
from typing import Tuple
class Webcam:
def __init__(self, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, fps=30) -> None:
self._vid = cv2.VideoCapture(0)
if not self._vid.isOpened():
raise Pyvidplayer2Error("Failed to find webcam.")
self.original_size = (int(self._vid.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._vid.get(cv2.CAP_PROP_FRAME_HEIGHT)))
self.current_size = self.original_size
self.aspect_ratio = self.original_size[0] / self.original_size[1]
self.frame_data = None
self.frame_surf = None
self.active = False
self.post_func = post_process
self.interp = interp
self.fps = fps
self._frame_delay = 1 / self.fps
self._frames = 0
self._last_tick = 0
self.play()
def __str__(self) -> str:
return f"<Webcam(fps={self.fps})>"
def _update(self) -> bool:
if self.active:
if time.time() - self._last_tick > self._frame_delay:
has_frame, data = self._vid.read()
if has_frame:
if self.original_size != self.current_size:
data = cv2.resize(data, dsize=self.current_size, interpolation=self.interp)
data = self.post_func(data)
self.frame_data = data
self.frame_surf = self._create_frame(data)
self._frames += 1
self._last_tick = time.time()
return True
return False
def play(self) -> None:
self.active = True
def stop(self) -> None:
self.active = False
self.frame_data = None
self.frame_surf = None
def resize(self, size: Tuple[int, int]) -> None:
self.current_size = size
def change_resolution(self, height: int) -> None:
self.current_size = (int(height * self.aspect_ratio), height)
def close(self) -> None:
self.stop()
self._vid.release()
def get_pos(self) -> float:
return self._frames / self.fps
def draw(self, surf, pos: Tuple[int, int], force_draw=True) -> bool:
if (self._update() or force_draw) and self.frame_surf is not None:
self._render_frame(surf, pos)
return True
return False
def _create_frame(self, data: numpy.ndarray) -> pygame.Surface:
return pygame.image.frombuffer(data.tobytes(), self.current_size, "BGR")
def _render_frame(self, surf: pygame.Surface, pos: Tuple[int, int]):
surf.blit(self.frame_surf, pos)
def preview(self) -> None:
win = pygame.display.set_mode(self.current_size)
pygame.display.set_caption(f"webcam")
self.play()
while self.active:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.stop()
pygame.time.wait(int(self._frame_delay * 1000))
self.draw(win, (0, 0), force_draw=False)
pygame.display.update()
pygame.display.quit()
self.close()

View File

@@ -0,0 +1,5 @@
numpy<1.25,>=1.21
opencv_python
pygame
pysubs2
PyAudio

27
pyvidplayer2/setup.py Normal file
View File

@@ -0,0 +1,27 @@
from setuptools import setup
from pyvidplayer2 import _VERSION
with open("README.md", 'r') as f:
long_desc = f.read()
setup(
name="pyvidplayer2",
version=_VERSION,
description="Video playback in Python",
long_description=long_desc,
long_description_content_type = "text/markdown",
author="Anray Liu",
author_email="anrayliu@gmail.com",
license="MIT",
packages=["pyvidplayer2"],
install_requires=["numpy<1.25,>=1.21",
"opencv_python",
"pygame",
"pysubs2",
"PyAudio"],
url="https://github.com/ree1261/pyvidplayer2",
platforms=["windows"],
keywords=["pygame", "video", "playback"],
)

132
utils.py Normal file
View File

@@ -0,0 +1,132 @@
from itertools import chain
import io
import os
import os.path
import zipfile
# import os
# import subprocess
def getMameResource(emulatorDir, typeofResource, is_dir):
resource = os.path.join(emulatorDir, typeofResource)
if is_dir:
if not os.path.isdir(resource):
return None
if os.path.isfile(os.path.join(resource, typeofResource + ".zip")):
resource = zipfile.ZipFile(os.path.join(resource, typeofResource + ".zip"))
else:
resource = os.path.join(resource, typeofResource)
else:
if not os.path.isfile(resource):
return None
with open(resource, "rt", encoding="latin1") as f:
resource = f.read()
return resource
# def aspect_scale(img, bx, by):
# """Scales 'img' to fit into box bx/by.
# This method will retain the original image's aspect ratio"""
# ix, iy = img.get_size()
# if ix > iy:
# # fit to width
# scale_factor = bx / float(ix)
# sy = scale_factor * iy
# if sy > by:
# scale_factor = by / float(iy)
# sx = scale_factor * ix
# sy = by
# else:
# sx = bx
# else:
# # fit to height
# scale_factor = by / float(iy)
# sx = scale_factor * ix
# if sx > bx:
# scale_factor = bx / float(ix)
# sx = bx
# sy = scale_factor * iy
# else:
# sy = by
# return pygame.transform.scale(img, (int(sx), int(sy)))
# def image_from_data(data, image_size):
# if data:
# with io.BytesIO(data) as f:
# surface = pygame.image.load(f)
# surface = aspect_scale(surface, image_size[0], image_size[1])
# else:
# surface = pygame.Surface(image_size) # pylint: disable=E1121
# return surface
# def addscanlines(surface):
# width = surface.get_width()
# for y in range(surface.get_height()):
# if y % 2:
# pygame.draw.line(surface, (0, 0, 0), (0, y), (width, y))
# return surface
def truncline(text, font, maxwidth):
real = len(text)
stext = text
size = font.size(text)[0]
cut = 0
a = 0
done = 1
while size > maxwidth:
a = a + 1
n = text.rsplit(None, a)[0]
if stext == n:
cut += 1
stext = n[:-cut]
else:
stext = n
size = font.size(stext)[0]
real = len(stext)
done = 0
return real, done, stext
def wrapline(text, font, maxwidth):
done = 0
wrapped = []
while not done:
nl, done, stext = truncline(text, font, maxwidth)
wrapped.append(stext.strip())
text = text[nl:]
return wrapped
def wrap_multi_line(text, font, maxwidth):
"""returns text taking new lines into account."""
if type(text) is str:
lines = chain(*(wrapline(line, font, maxwidth) for line in text.splitlines()))
else:
lines = chain(*(wrapline(line, font, maxwidth) for line in text))
return list(lines)
# def run_emulator(emu_type, exe, rom):
# old_path = os.getcwd()
# os.chdir(os.path.split(exe)[0])
# if emu_type == 'MAME':
# subprocess.run([exe, rom])
# elif emu_type == 'MESS':
# subprocess.run([exe, rom])
# elif emu_type == 'CSV':
# subprocess.run([exe.replace('<romname>', rom)])
# os.chdir(old_path)
# return True