Browse Source

Add PlatformIO configuration files and build artifacts for Arduino project

- Created .vscode/c_cpp_properties.json for IntelliSense configuration
- Added .vscode/launch.json for debugging setup
- Included build artifacts for main.cpp and Arduino variant
master
scayac 3 weeks ago
parent
commit
367cdcb070
  1. 5
      arduino/.gitignore
  2. 1
      arduino/.pio/build/project.checksum
  3. BIN
      arduino/.pio/build/uno/.sconsign312.dblite
  4. BIN
      arduino/.pio/build/uno/FrameworkArduino/CDC.cpp.o
  5. BIN
      arduino/.pio/build/uno/FrameworkArduino/HardwareSerial.cpp.o
  6. BIN
      arduino/.pio/build/uno/FrameworkArduino/HardwareSerial0.cpp.o
  7. BIN
      arduino/.pio/build/uno/FrameworkArduino/HardwareSerial1.cpp.o
  8. BIN
      arduino/.pio/build/uno/FrameworkArduino/HardwareSerial2.cpp.o
  9. BIN
      arduino/.pio/build/uno/FrameworkArduino/HardwareSerial3.cpp.o
  10. BIN
      arduino/.pio/build/uno/FrameworkArduino/IPAddress.cpp.o
  11. BIN
      arduino/.pio/build/uno/FrameworkArduino/PluggableUSB.cpp.o
  12. BIN
      arduino/.pio/build/uno/FrameworkArduino/Print.cpp.o
  13. BIN
      arduino/.pio/build/uno/FrameworkArduino/Stream.cpp.o
  14. BIN
      arduino/.pio/build/uno/FrameworkArduino/Tone.cpp.o
  15. BIN
      arduino/.pio/build/uno/FrameworkArduino/USBCore.cpp.o
  16. BIN
      arduino/.pio/build/uno/FrameworkArduino/WInterrupts.c.o
  17. BIN
      arduino/.pio/build/uno/FrameworkArduino/WMath.cpp.o
  18. BIN
      arduino/.pio/build/uno/FrameworkArduino/WString.cpp.o
  19. BIN
      arduino/.pio/build/uno/FrameworkArduino/abi.cpp.o
  20. BIN
      arduino/.pio/build/uno/FrameworkArduino/hooks.c.o
  21. BIN
      arduino/.pio/build/uno/FrameworkArduino/main.cpp.o
  22. BIN
      arduino/.pio/build/uno/FrameworkArduino/new.cpp.o
  23. BIN
      arduino/.pio/build/uno/FrameworkArduino/wiring.c.o
  24. BIN
      arduino/.pio/build/uno/FrameworkArduino/wiring_analog.c.o
  25. BIN
      arduino/.pio/build/uno/FrameworkArduino/wiring_digital.c.o
  26. BIN
      arduino/.pio/build/uno/FrameworkArduino/wiring_pulse.S.o
  27. BIN
      arduino/.pio/build/uno/FrameworkArduino/wiring_pulse.c.o
  28. BIN
      arduino/.pio/build/uno/FrameworkArduino/wiring_shift.c.o
  29. BIN
      arduino/.pio/build/uno/firmware.elf
  30. 279
      arduino/.pio/build/uno/firmware.hex
  31. 1
      arduino/.pio/build/uno/idedata.json
  32. BIN
      arduino/.pio/build/uno/libFrameworkArduino.a
  33. 1
      arduino/.pio/build/uno/libFrameworkArduinoVariant.a
  34. BIN
      arduino/.pio/build/uno/src/main.cpp.o
  35. 58
      arduino/.vscode/c_cpp_properties.json
  36. 44
      arduino/.vscode/launch.json
  37. 0
      backend/app/__init__.py
  38. 34
      frontend/README_FRONTEND.md
  39. 36
      frontend/app/__init__.py
  40. BIN
      frontend/app/__pycache__/__init__.cpython-312.pyc
  41. BIN
      frontend/app/__pycache__/backend_client.cpython-312.pyc
  42. BIN
      frontend/app/__pycache__/routes.cpython-312.pyc
  43. 328
      frontend/app/routes.py
  44. 288
      frontend/app/templates/audio_storage.html
  45. 54
      frontend/app/templates/base.html
  46. 1
      frontend/requirements.txt

5
arduino/.gitignore vendored

@ -1,5 +0,0 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

1
arduino/.pio/build/project.checksum

@ -0,0 +1 @@
dbe76af323ba99fa5d31228003a40ed9c8f93bc9

BIN
arduino/.pio/build/uno/.sconsign312.dblite

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/CDC.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/HardwareSerial.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/HardwareSerial0.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/HardwareSerial1.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/HardwareSerial2.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/HardwareSerial3.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/IPAddress.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/PluggableUSB.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/Print.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/Stream.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/Tone.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/USBCore.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/WInterrupts.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/WMath.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/WString.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/abi.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/hooks.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/main.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/new.cpp.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/wiring.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/wiring_analog.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/wiring_digital.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/wiring_pulse.S.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/wiring_pulse.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/FrameworkArduino/wiring_shift.c.o

Binary file not shown.

BIN
arduino/.pio/build/uno/firmware.elf

Binary file not shown.

279
arduino/.pio/build/uno/firmware.hex

@ -0,0 +1,279 @@
:100000000C94D4000C94FC000C94FC000C94FC00A8
:100010000C94FC000C94FC000C94FC000C94FC0070
:100020000C94FC000C94FC000C9435040C94A40477
:100030000C94FC000C94FC000C94FC000C94FC0050
:100040000C94FC000C94FC000C94FC000C94FC0040
:100050000C94FC000C94FC000C94FC000C94EB033E
:100060000C94FC000C94FC000C94FC000C94FC0020
:100070000C94FC000C94FC000C94FC000C94FC0010
:100080000C94FC000C94FC000C94FC000C94FC0000
:100090000C94FC000C94FC000C94FC000C94FC00F0
:1000A0000C94FC000C94FC000C94FC00080B000267
:1000B0000202000009040000010202000005240001
:1000C0001001052401010104240206052406000193
:1000D0000705810310004009040100020A00000026
:1000E00007050202400000070583024000000403E8
:1000F000090412010002EF02014041233780000190
:100100000102030141726475696E6F204C4C43001B
:1001100041726475696E6F204D6963726F000000F3
:100120000000250028002B002E00310000000000F8
:10013000240027002A002D003000000000002300CA
:10014000260029002C002F000404040404030405E5
:100150000202020204030202020206060606060664
:10016000040402020204040408020110408040104A
:1001700020408040800802040180402010020110CD
:100180008010204040200000000200090F00000302
:100190000401000C0000000000000000000000004E
:1001A0000000000000004D0811241FBECFEFDAE070
:1001B000DEBFCDBF11E0A0E0B1E0E8E1F1E102C0B7
:1001C00005900D92A233B107D9F721E0A2E3B1E087
:1001D00001C01D92A93BB207E1F710E0C4EDD0E0E9
:1001E00004C02197FE010E948408C33DD107C9F7CE
:1001F0000E94D6060C948A080C940000FC018091A1
:100200006801882311F13FB7F89482E08093E900F8
:100210002091F200822F90E01816190614F481E064
:1002200090E0882339F0289A44E640936701409192
:10023000F1004083222339F02091F200211103C004
:100240002BE62093E8003FBF08958FEF9FEF0895BE
:100250002FB7F89483E08093E9009091E800892F0C
:10026000807295FF04C09091F20080E4891B2FBF3B
:10027000089580915F0181110DC082E080935B0140
:1002800084E080935C0110925E0110925D0181E038
:1002900080935F018BE591E00895282F30E0F9010C
:1002A000EA57FE4F9491F901E959FE4F8491285B7A
:1002B0003E4FF9012491222309F45BC09923E9F010
:1002C00091509F30D0F4E92FF0E0E759FE4F0C94A5
:1002D00084089401980178018C0190017D017D01D1
:1002E0007D019B01A101A501A901AF017D01B30120
:1002F000909180009F7790938000E22FF0E0EE0FC6
:10030000FF1FE65CFE4FA591B491EC91E82381E0DC
:1003100090E089F580E00895909180009F7DEBCF7B
:1003200090918000977FE7CF94B59F7794BDE5CFFC
:1003300094B59F7DFBCF909190009F779093900014
:10034000DCCF909190009F7DF9CF90919000977FA6
:10035000F5CF9091C0009F779093C000CECF909141
:10036000C0009F7DF9CF9091C200977F9093C2000B
:10037000C4CF80E090E008953FB7F8948091570192
:1003800090915801A0915901B0915A0126B5A89BAE
:1003900005C02F3F19F00196A11DB11D3FBFBA2F17
:1003A000A92F982F8827BC01CD01620F711D811DD7
:1003B000911D42E0660F771F881F991F4A95D1F75C
:1003C00008958F929F92AF92BF92CF92DF92EF9259
:1003D000FF924B015C010E94BC016B017C010E94F9
:1003E000BC016C197D098E099F09683E7340810527
:1003F0009105A8F328EEC20E23E0D21EE11CF11CE9
:100400008A9489288A288B2829F0812C912C5401E0
:100410008394E5CFFF90EF90DF90CF90BF90AF90A7
:100420009F908F9008954091340150913501209113
:1004300032013091330142175307B4F49091E80030
:100440009570E1F39091E80092FD19C08093F1005E
:10045000809134019091350101968F739927892BF2
:1004600019F48EEF8093E80080913401909135016A
:100470000196909335018093340181E0089580E0E6
:100480000895EF92FF920F931F93CF93DF93F82E6F
:10049000192FE62E042F81E0860F880F0E94130289
:1004A00083E00E941302CF2DD12FEC0EFD2EF11C04
:1004B000CE15DF05B9F007FF13C0FE0184910E943D
:1004C0001302182F80E00E941302812321968111CC
:1004D000EFCFDF91CF911F910F91FF90EF90089593
:1004E0008881EDCF81E0F5CFDF92EF92FF920F93FD
:1004F0001F93CF93DF93D82E8A01EB017B01E40E8B
:10050000F51ECE15DF0559F0D7FE12C0FE0184910D
:100510000E94130221968111F4CF0FEF1FEFC80143
:10052000DF91CF911F910F91FF90EF90DF90089591
:100530008881EECF0F931F93CF93DF931F92CDB798
:10054000DEB782E0898342E450E06CEA70E080E844
:100550000E9474020E943901DC0112960D911C91D7
:100560000115110589F0D801ED91FC910280F3810C
:10057000E02DBE016F5F7F4FC801099597FD04C054
:10058000F80100851185ECCF89810F90DF91CF9123
:100590001F910F910895615030F02091F100FC01FE
:1005A00020830196F8CF289A84E680936701089506
:1005B0008F929F92AF92BF92CF92DF92EF92FF9273
:1005C0000F931F93CF93DF936C017B018A0180917E
:1005D0000B01882309F45CC080916801882309F429
:1005E00057C08091380180FF05C08091E000826093
:1005F0008093E000E801B12C8AEFA82E93E0892EC9
:100600002AE3922E209711F4BB20D9F10E942801F1
:1006100081110AC0AA94AA20D9F161E070E080E0BB
:1006200090E00E94E101EECF8C171D0611F00CF056
:100630008C2F9FB7F8948092E9002091E80025FD67
:1006400002C09FBFDFCF282F30E0C21BD30BF701C2
:10065000815020F041914093F100FACFE20EF31E59
:10066000BB2021F09092E800B12CEBCF8091E80004
:1006700085FDE7CF9092E800BB24B394209709F35F
:10068000F3CF5D9A84E680933701101611063CF093
:1006900081E090E0F6019383828310E000E0C801DE
:1006A000DF91CF911F910F91FF90EF90DF90CF904E
:1006B000BF90AF909F908F900895CF93DF931F923C
:1006C000CDB7DEB76983DC01ED91FC910280F38147
:1006D000E02D41E050E0BE016F5F7F4F09950F9024
:1006E000DF91CF91089583E08093E9008091F2003B
:1006F000882319F08AE38093E80008950E94280176
:1007000090E00895CF93DF931F92CDB7DEB7FC0141
:100710008485958597FD08C02FEF3FEF35872487A7
:100720000F90DF91CF910895CE0101960E94FE00B7
:10073000019719F4898190E0F3CF8FEF9FEFF0CF0D
:100740000F931F93CF93DF931F92CDB7DEB78C012A
:10075000FC018485958597FF0BC0CE0101960E9410
:10076000FE00019771F4898190E0F80195878487F4
:10077000F801848595850F90DF91CF911F910F919E
:1007800008958FEF9FEFF1CFFC018485958597FD4C
:100790000BC09FB7F89482E08093E9008091F2004B
:1007A0009FBF90E0019608959FB7F89482E08093F0
:1007B000E9008091F2009FBF90E00895FC01019054
:1007C0000020E9F73197AF01481B590BBC0189E6BE
:1007D00091E00C94D8021F920F920FB60F92112441
:1007E0002F933F938F939F93AF93BF938091530128
:1007F00090915401A0915501B09156013091520150
:1008000023E0230F2D3758F50196A11DB11D20932C
:1008100052018093530190935401A0935501B093DA
:1008200056018091570190915801A0915901B091C2
:100830005A010196A11DB11D809357019093580153
:10084000A0935901B0935A01BF91AF919F918F919D
:100850003F912F910F900FBE0F901F90189526E893
:10086000230F0296A11DB11DD2CF1F920F920FB67A
:100870000F9211248F939F938091E1009091E1005A
:10088000937F9093E10083FF0FC01092E90091E005
:100890009093EB001092EC0092E39093ED00109295
:1008A000680198E09093F00082FF22C093E090935B
:1008B000E9009091F200992319F09AE39093E800EF
:1008C00090913701992341F0909137019150909385
:1008D0003701911101C05D9890916701992341F012
:1008E00090916701915090936701911101C02898F0
:1008F00084FF18C08091E2008E7E81608093E200C8
:100900008091E1008F7E8093E100809138018E7E9E
:100910008061809338019F918F910F900FBE0F904F
:100920001F90189580FFF7CF8091E2008E7E806146
:100930008093E2008091E1008E7E8093E1008091BF
:1009400038018E7E8160E5CF1F920F920FB60F9215
:100950001124CF92DF92EF92FF920F931F932F9368
:100960003F934F935F936F937F938F939F93AF9337
:10097000BF93EF93FF93CF93DF93CDB7DEB76C9721
:10098000DEBFCDBF1092E9008091E80083FF25C053
:1009900068E0CE0145960E94CB0282EF8093E8008A
:1009A0008D8987FF39C09091E80090FFFCCF982F88
:1009B000907609F034C19E894F89588D2F89F88C23
:1009C000911131C0803861F5809139018093F10037
:1009D0001092F1008EEF8093E8006C960FB6F894B9
:1009E000DEBF0FBECDBFDF91CF91FF91EF91BF91E1
:1009F000AF919F918F917F916F915F914F913F91B7
:100A00002F911F910F91FF90EF90DF90CF900F905B
:100A10000FBE0F901F9018959EEF9093E800C7CFE0
:100A20001092F100D5CF913059F48111D3CF4130DC
:100A3000510581F6809139018D7F80933901CACFAC
:100A4000933049F48111C6CF4130510519F6809198
:100A500039018260F2CF953041F48091E80080FF47
:100A6000FCCF20682093E300B5CF963009F0A9C0F1
:100A70000B8D1C8D22E01092E9001092350110922E
:100A80003401F2122EC010923301109232010E94F2
:100A90009A021F8299E09983FA8291E09E8390EAFC
:100AA00098879AEF99872091340130913501275F1B
:100AB0003F4F3C832B838D831092E90010923501C8
:100AC00010923401109333010093320149E050E059
:100AD000BE016F5F7F4F80E00E9474020E949A0205
:100AE00079CF10933301009332010E943901DC0168
:100AF00012960D911C910115110509F451C1D801EF
:100B0000ED91FC910480F581E02DBE016B5E7F4F7D
:100B1000C8010995009709F03EC1F80100851185CB
:100B2000EACFF3E0FF120EC08F89882309F440C09A
:100B3000823061F440E86DE080E191E00E94410282
:100B4000811148CF81E28093EB0047CF813029F4B7
:100B500040E86BE084E091E0F1CF833099F70E94A8
:100B60003901DC011296ED90FC908E010F5F1F4F52
:100B70006801E114F10479F0D701ED91FC91068050
:100B8000F781E02DB801C7010995080F111DF70184
:100B9000E084F184EECFD8011C92F6010190002090
:100BA000E9F73197BF016C197D0940E0C601C6CF56
:100BB0006EEE70E0FB01449150E080E80E94740208
:100BC00009CF973009F4BECF983021F481E08093AB
:100BD000F10000CF993009F0FDCE837009F0B2CF5B
:100BE000EFE1F1E081E031E096E32191222371F021
:100BF0008093E9003093EB00DF0111972C91209353
:100C0000EC009093ED008F5F873079F78EE780934B
:100C1000EA001092EA008F8980936801DBCE8B8D09
:100C20009C8D1092E900109235011092340190933E
:100C3000330180933201898D811192C08E899D8903
:100C4000913A49F4813209F07DCF47E050E064E009
:100C500071E080E0B3CF913209F074CF833269F450
:100C60008F89988DB0E0A0E08093000190930101FE
:100C7000A0930201B0930301ADCE803269F480915C
:100C8000E80082FFFCCF67E084E091E00E94CB02A5
:100C90008BEF8093E8009ECE823209F09BCE8F8945
:100CA00080930B01EEEFFFE7859194918B3F9C4D74
:100CB00051F1E0E0F8E08091040190910501A091EC
:100CC0000601B0910701803B9440A105B105F1F404
:100CD00080910B0180FD1AC0EE3F8AE0F80789F58C
:100CE00087E797E791838083809160008093360146
:100CF00088E19BE00FB6F894A895809360000FBE42
:100D00009093600067CEEEEFFAE0D5CF80819181BD
:100D10008737974709F05ECEA8958091600088617B
:100D2000809360008091360180936000EE3F2AE05E
:100D3000F20789F08091FE0A9091FF0A91838083E7
:100D400049CE808191818737980751F29093FF0AAD
:100D50008093FE0AC5CF1092FF0A1092FE0A3ACE87
:100D60000E943901DC0112960D911C9101151105AB
:100D700009F4E8CED801ED91FC910190F081E02DCD
:100D8000BE016B5E7F4FC8010995811123CEF8012A
:100D900000851185EBCF181619060CF41BCED2CEA8
:100DA000F1E0FF12BECE62EF70E004CFCF93DF938D
:100DB000CDB7DEB7A5970FB6F894DEBF0FBECDBF97
:100DC000789484B5826084BD84B5816084BD85B526
:100DD000826085BD85B5816085BD80916E00816032
:100DE00080936E00109281008091810082608093D8
:100DF00081008091810081608093810080918000DA
:100E00008160809380008091910082608093910046
:100E10008091910081608093910080919000816029
:100E2000809390008091C10084608093C100809184
:100E3000C10082608093C1008091C1008160809375
:100E4000C1008091C30081608093C3008091C00085
:100E500082608093C0008091C20081608093C20054
:100E600080917A00846080937A0080917A00826019
:100E700080937A0080917A00816080937A008091DB
:100E80007A00806880937A0010926801109239018C
:100E9000109238018091D70081608093D70080EA5A
:100EA0008093D80089B5806189BD89B5826089BD8C
:100EB00009B400FEFDCF61E070E080E090E00E94A8
:100EC000E1018091D8008F7C80618093D80080916F
:100ED000E000807F8093E0008091E1008E7E80932F
:100EE000E1008DE08093E200559A209A5D98289861
:100EF0008FEF9FEF90937601809375010AE311E0E5
:100F000055E2E52E51E0F52ED7018D917D01282F78
:100F100030E0F901E959FE4F9491285B3E4FF90109
:100F200024912223C9F030E0220F331FF901E45D40
:100F3000FE4FA591B491F901E25EFE4F45915491A7
:100F40003FB7F8946C91292F209526232C93DA0132
:100F5000EC919E2B9C933FBF0E944D0121E0892B79
:100F600009F420E03FB7F894809153019091540127
:100F7000A0915501B09156013FBFF8012083218314
:100F800082839383A483B5830A5F1F4FF9E2EF1630
:100F9000F1E0FF0609F0B8CF9AE0492E512C612C00
:100FA000712C8FB7F8948090530190905401A090C9
:100FB0005501B09056018FBF0AE311E085E2282E5B
:100FC00081E0382ED101DD901D018D2D0E944D0153
:100FD0009C01CC24C394892B09F4C12CF801808195
:100FE000C81629F0C08282829382A482B582D80179
:100FF00012964D915D916D917C911597D501C4012B
:10100000841B950BA60BB70B8897A105B10588F13A
:10101000D80111968C911197C81659F11196CC925E
:10102000882339F1232B29F589E291E00E94DE0320
:101030002D2D30E050E040E019A26E01B1E2CB0E60
:10104000D11CCA01B901A30192010E946208605D2E
:10105000F60162936F01211531054105510589F7AC
:10106000309719F0CF010E94DE038EE291E00E94DA
:10107000DE030A5F1F4FE214F30409F0A3CF62E01E
:1010800070E080E090E00E94E10140E050E0452BFC
:1010900009F487CF0E94000084CFE9E6F1E01382D3
:1010A000128288EE93E0A0E0B0E084839583A6836B
:1010B000B78380E191E0918380838FEF9FEF9587E5
:1010C00084870895A1E21A2EAA1BBB1BFD010DC047
:1010D000AA1FBB1FEE1FFF1FA217B307E407F507E8
:1010E00020F0A21BB30BE40BF50B661F771F881FC4
:1010F000991F1A9469F760957095809590959B015A
:10110000AC01BD01CF010895EE0FFF1F0590F491D2
:08111000E02D0994F894FFCFD3
:10111800FFFFFFFF00E100000000000000000000EA
:101128005D03D8027E037303C4038203A00300C1D6
:101138008081000000030507094750494F000D0A48
:021148000000A5
:00000001FF

1
arduino/.pio/build/uno/idedata.json

@ -0,0 +1 @@
{"build_type": "release", "env_name": "uno", "libsource_dirs": ["/home/christophe/Bureau/Prog/pySonnerie/arduino/lib", "/home/christophe/Bureau/Prog/pySonnerie/arduino/.pio/libdeps/uno", "/home/christophe/.platformio/lib", "/home/christophe/.platformio/packages/framework-arduino-avr/libraries"], "defines": ["PLATFORMIO=60119", "ARDUINO_AVR_MICRO", "F_CPU=16000000L", "ARDUINO_ARCH_AVR", "ARDUINO=10808", "USB_VID=0x2341", "USB_PID=0x8037", "USB_PRODUCT=\"Arduino Micro\"", "USB_MANUFACTURER=\"Arduino\"", "__AVR_ATmega32U4__"], "includes": {"build": ["/home/christophe/Bureau/Prog/pySonnerie/arduino/src", "/home/christophe/.platformio/packages/framework-arduino-avr/cores/arduino", "/home/christophe/.platformio/packages/framework-arduino-avr/variants/micro"], "compatlib": ["/home/christophe/.platformio/packages/framework-arduino-avr/libraries/EEPROM/src", "/home/christophe/.platformio/packages/framework-arduino-avr/libraries/HID/src", "/home/christophe/.platformio/packages/framework-arduino-avr/libraries/SPI/src", "/home/christophe/.platformio/packages/framework-arduino-avr/libraries/SoftwareSerial/src", "/home/christophe/.platformio/packages/framework-arduino-avr/libraries/Wire/src"], "toolchain": ["/home/christophe/.platformio/packages/toolchain-atmelavr/lib/gcc/avr/7.3.0/include-fixed", "/home/christophe/.platformio/packages/toolchain-atmelavr/lib/gcc/avr/7.3.0/include", "/home/christophe/.platformio/packages/toolchain-atmelavr/avr/include"]}, "cc_flags": ["-std=gnu11", "-fno-fat-lto-objects", "-mmcu=atmega32u4", "-Os", "-Wall", "-ffunction-sections", "-fdata-sections", "-flto"], "cxx_flags": ["-fno-exceptions", "-fno-threadsafe-statics", "-fpermissive", "-std=gnu++11", "-mmcu=atmega32u4", "-Os", "-Wall", "-ffunction-sections", "-fdata-sections", "-flto"], "cc_path": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin/avr-gcc", "cxx_path": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin/avr-g++", "gdb_path": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin/avr-gdb", "prog_path": "/home/christophe/Bureau/Prog/pySonnerie/arduino/.pio/build/uno/firmware.elf", "svd_path": null, "compiler_type": "gcc", "targets": [{"name": "size", "title": "Program Size", "description": "Calculate program size", "group": "Platform"}, {"name": "upload", "title": "Upload", "description": null, "group": "Platform"}, {"name": "uploadeep", "title": "Upload EEPROM", "description": null, "group": "Platform"}, {"name": "fuses", "title": "Set Fuses", "description": null, "group": "Platform"}, {"name": "bootloader", "title": "Burn Bootloader", "description": null, "group": "Platform"}], "extra": {"flash_images": []}}

BIN
arduino/.pio/build/uno/libFrameworkArduino.a

Binary file not shown.

1
arduino/.pio/build/uno/libFrameworkArduinoVariant.a

@ -0,0 +1 @@
!<arch>

BIN
arduino/.pio/build/uno/src/main.cpp.o

Binary file not shown.

58
arduino/.vscode/c_cpp_properties.json vendored

@ -0,0 +1,58 @@
//
// !!! WARNING !!! AUTO-GENERATED FILE!
// PLEASE DO NOT MODIFY IT AND USE "platformio.ini":
// https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags
//
{
"configurations": [
{
"name": "PlatformIO",
"includePath": [
"/home/christophe/Bureau/Prog/pySonnerie/arduino/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/cores/arduino",
"/home/christophe/.platformio/packages/framework-arduino-avr/variants/micro",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/EEPROM/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/HID/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/SPI/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/SoftwareSerial/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/Wire/src",
""
],
"browse": {
"limitSymbolsToIncludedHeaders": true,
"path": [
"/home/christophe/Bureau/Prog/pySonnerie/arduino/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/cores/arduino",
"/home/christophe/.platformio/packages/framework-arduino-avr/variants/micro",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/EEPROM/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/HID/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/SPI/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/SoftwareSerial/src",
"/home/christophe/.platformio/packages/framework-arduino-avr/libraries/Wire/src",
""
]
},
"defines": [
"PLATFORMIO=60119",
"ARDUINO_AVR_MICRO",
"F_CPU=16000000L",
"ARDUINO_ARCH_AVR",
"ARDUINO=10808",
"USB_VID=0x2341",
"USB_PID=0x8037",
"USB_PRODUCT=\"Arduino Micro\"",
"USB_MANUFACTURER=\"Arduino\"",
"__AVR_ATmega32U4__",
""
],
"cStandard": "gnu11",
"cppStandard": "gnu++11",
"compilerPath": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin/avr-gcc",
"compilerArgs": [
"-mmcu=atmega32u4",
""
]
}
],
"version": 4
}

44
arduino/.vscode/launch.json vendored

@ -0,0 +1,44 @@
// AUTOMATICALLY GENERATED FILE. PLEASE DO NOT MODIFY IT MANUALLY
//
// PlatformIO Debugging Solution
//
// Documentation: https://docs.platformio.org/en/latest/plus/debugging.html
// Configuration: https://docs.platformio.org/en/latest/projectconf/sections/env/options/debug/index.html
{
"version": "0.2.0",
"configurations": [
{
"type": "platformio-debug",
"request": "launch",
"name": "PIO Debug",
"executable": "/home/christophe/Bureau/Prog/pySonnerie/arduino/.pio/build/uno/firmware.elf",
"projectEnvName": "uno",
"toolchainBinDir": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin",
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": {
"type": "PlatformIO",
"task": "Pre-Debug"
}
},
{
"type": "platformio-debug",
"request": "launch",
"name": "PIO Debug (skip Pre-Debug)",
"executable": "/home/christophe/Bureau/Prog/pySonnerie/arduino/.pio/build/uno/firmware.elf",
"projectEnvName": "uno",
"toolchainBinDir": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin",
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "platformio-debug",
"request": "launch",
"name": "PIO Debug (without uploading)",
"executable": "/home/christophe/Bureau/Prog/pySonnerie/arduino/.pio/build/uno/firmware.elf",
"projectEnvName": "uno",
"toolchainBinDir": "/home/christophe/.platformio/packages/toolchain-atmelavr/bin",
"internalConsoleOptions": "openOnSessionStart",
"loadMode": "manual"
}
]
}

0
backend/app/__init__.py

34
frontend/README_FRONTEND.md

@ -9,6 +9,34 @@ Frontend web responsive en Flask pour piloter le backend pySonnerie déjà en pl
- Lancement manuel d'un trigger (`/api/play/{trigger_id}`) - Lancement manuel d'un trigger (`/api/play/{trigger_id}`)
- Arrêt audio (`/api/stop`) - Arrêt audio (`/api/stop`)
- Gestion du stockage audio dans `backend/data/musiques` (televersement, telechargement, suppression) - Gestion du stockage audio dans `backend/data/musiques` (televersement, telechargement, suppression)
- Import audio depuis un lien YouTube (extraction via `yt-dlp` puis conversion en MP3)
## Prerequis
- Python 3.11+
- `yt-dlp` disponible dans l'environnement d'execution (installe via `pip install -r requirements.txt`)
- `ffmpeg` installe sur le systeme (requis par `yt-dlp` pour l'extraction audio)
- `node` installe sur le systeme (requis pour `--js-runtimes node`)
- Acces reseau sortant vers YouTube et GitHub (utilise par `--remote-components ejs:github`)
- Droits d'ecriture sur `backend/data/musiques`
Installation recommandee (Debian/Ubuntu):
```bash
sudo apt update
sudo apt install ffmpeg nodejs
```
Verification rapide des prerequis:
```bash
source .venv/bin/activate
yt-dlp --version
node --version
ffmpeg -version
```
Note Debian/Ubuntu: si `node --version` echoue mais `nodejs --version` fonctionne, cree un alias ou un lien symbolique `node` vers `nodejs` pour respecter la commande d'extraction.
## Installation ## Installation
@ -99,6 +127,12 @@ Le frontend sera alors servi par Gunicorn sur l'adresse definie par `FRONTEND_BI
- Le frontend appelle le backend en HTTPS avec certificat autosigne (`verify=False`). - Le frontend appelle le backend en HTTPS avec certificat autosigne (`verify=False`).
- Les fichiers audio sont manipules localement dans `backend/data/musiques`. - Les fichiers audio sont manipules localement dans `backend/data/musiques`.
- Formats audio acceptes: `.mp3`, `.wav`, `.ogg`, `.flac`, `.aac`, `.m4a`. - Formats audio acceptes: `.mp3`, `.wav`, `.ogg`, `.flac`, `.aac`, `.m4a`.
- Depuis la page de stockage audio, un champ URL permet d'importer l'audio d'une video YouTube.
- La commande utilisee pour l'extraction est:
```bash
yt-dlp --js-runtimes node --remote-components ejs:github -x --audio-format mp3 URL
```
## Changelog ## Changelog

36
frontend/app/__init__.py

@ -1,36 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
from flask import Flask
def _load_secret_key(project_root: Path) -> str:
conf_path = project_root / "frontend" / "data" / "conf.json"
if conf_path.exists():
try:
conf = json.loads(conf_path.read_text(encoding="utf-8"))
key = conf.get("secret_key", "")
if key:
return str(key)
except Exception:
pass
return "pysonnerie-frontend-dev-key"
def create_app() -> Flask:
app = Flask(__name__)
project_root = Path(__file__).resolve().parents[2]
secret = _load_secret_key(project_root)
app.config["SECRET_KEY"] = secret
app.config["PROJECT_ROOT"] = project_root
app.config["MUSIC_DIR"] = project_root / "backend" / "data" / "musiques"
app.config["MAX_CONTENT_LENGTH"] = 128 * 1024 * 1024
from .routes import ui
app.register_blueprint(ui)
return app

BIN
frontend/app/__pycache__/__init__.cpython-312.pyc

Binary file not shown.

BIN
frontend/app/__pycache__/backend_client.cpython-312.pyc

Binary file not shown.

BIN
frontend/app/__pycache__/routes.cpython-312.pyc

Binary file not shown.

328
frontend/app/routes.py

@ -1,6 +1,10 @@
from __future__ import annotations from __future__ import annotations
import re import re
import shutil
import subprocess
import threading
import uuid
from pathlib import Path from pathlib import Path
from flask import ( from flask import (
@ -25,6 +29,19 @@ ui = Blueprint("ui", __name__, template_folder="templates")
GPIO_PATTERN = re.compile(r"^GPIO\d+$") GPIO_PATTERN = re.compile(r"^GPIO\d+$")
ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a"} ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a"}
YOUTUBE_PROGRESS_PATTERN = re.compile(r"(\d+(?:\.\d+)?)%")
YOUTUBE_POSTPROCESS_HINTS = (
"extractaudio",
"ffmpeg",
"postprocess",
"post-process",
"merging",
"deleting original file",
"destination",
)
INVALID_FILENAME_CHARS_PATTERN = re.compile(r"[<>:\"/\\|?*\x00-\x1f]")
_YOUTUBE_JOBS_LOCK = threading.Lock()
_YOUTUBE_JOBS: dict[str, dict[str, object]] = {}
def _backend_client() -> BackendClient | None: def _backend_client() -> BackendClient | None:
@ -58,6 +75,17 @@ def _list_audio_files() -> list[str]:
return sorted(files, key=str.lower) return sorted(files, key=str.lower)
def _used_audio_files_from_triggers(triggers: dict[str, dict]) -> set[str]:
used: set[str] = set()
for trigger in triggers.values():
if not isinstance(trigger, dict):
continue
music_file = str(trigger.get("music_file", "")).strip()
if music_file:
used.add(Path(music_file).name.lower())
return used
def _parse_optional_float(value: str) -> float | None: def _parse_optional_float(value: str) -> float | None:
clean = value.strip() clean = value.strip()
if clean == "": if clean == "":
@ -83,6 +111,170 @@ def _audio_redirect_target() -> str:
return url_for("ui.dashboard") return url_for("ui.dashboard")
def _normalize_youtube_filename(raw_name: str) -> str | None:
# Keep readable names (including spaces) but strip dangerous/path chars.
candidate = Path(raw_name.strip()).name
if candidate == "":
return None
stem = Path(candidate).stem
stem = INVALID_FILENAME_CHARS_PATTERN.sub(" ", stem)
stem = re.sub(r"\s+", " ", stem).strip(" .")
if stem == "":
return None
return f"{stem}.mp3"
def _next_available_filename(filename: str, existing_names: set[str]) -> str:
candidate = filename
stem = Path(filename).stem
suffix = Path(filename).suffix or ".mp3"
index = 2
while candidate.lower() in existing_names:
candidate = f"{stem}_{index}{suffix}"
index += 1
return candidate
def _build_youtube_download_command(youtube_url: str, requested_name: str, music_dir: Path) -> tuple[list[str] | None, str | None]:
yt_dlp_path = shutil.which("yt-dlp")
if yt_dlp_path is None:
return None, "yt-dlp est introuvable sur le système."
if shutil.which("node") is None:
return None, "node est requis pour l'option --js-runtimes node."
command = [
yt_dlp_path,
"--js-runtimes",
"node",
"--remote-components",
"ejs:github",
"--newline",
"-x",
"--audio-format",
"mp3",
]
if requested_name:
normalized_name = _normalize_youtube_filename(requested_name)
if normalized_name is None:
return None, "Nom de fichier YouTube invalide."
existing_names = {item.name.lower() for item in music_dir.iterdir() if item.is_file()}
if normalized_name.lower() in existing_names:
return None, "Un fichier avec ce nom existe déjà."
output_template = music_dir / f"{Path(normalized_name).stem}.%(ext)s"
command.extend(["-o", str(output_template)])
else:
command.extend(["-P", str(music_dir)])
command.append(youtube_url)
return command, None
def _extract_progress_percent(line: str) -> float | None:
match = YOUTUBE_PROGRESS_PATTERN.search(line)
if match is None:
return None
try:
value = float(match.group(1))
except ValueError:
return None
return max(0.0, min(100.0, value))
def _set_youtube_job_state(job_id: str, **updates: object) -> None:
with _YOUTUBE_JOBS_LOCK:
current = _YOUTUBE_JOBS.get(job_id)
if current is None:
return
current.update(updates)
def _create_youtube_job() -> str:
job_id = uuid.uuid4().hex
with _YOUTUBE_JOBS_LOCK:
_YOUTUBE_JOBS[job_id] = {
"status": "running",
"percent": 0.0,
"message": "Préparation du téléchargement...",
}
return job_id
def _run_youtube_download_job(job_id: str, command: list[str]) -> None:
try:
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
except OSError as exc:
_set_youtube_job_state(job_id, status="error", message=f"Impossible d'exécuter yt-dlp: {exc}")
return
last_line = ""
max_percent = 0.0
in_postprocess = False
output = process.stdout
if output is not None:
for raw_line in output:
line = raw_line.strip()
if line == "":
continue
last_line = line
lower_line = line.lower()
if any(hint in lower_line for hint in YOUTUBE_POSTPROCESS_HINTS):
in_postprocess = True
shown_percent = max(max_percent, 99.0)
_set_youtube_job_state(
job_id,
status="running",
percent=shown_percent,
message="Conversion audio en cours...",
)
progress = _extract_progress_percent(line)
if progress is None:
continue
if progress > max_percent:
max_percent = progress
shown_percent = min(max_percent, 99.0)
if shown_percent >= 99.0:
in_postprocess = True
message = (
"Conversion audio en cours..."
if in_postprocess
else f"Téléchargement en cours: {shown_percent:.1f}%"
)
_set_youtube_job_state(
job_id,
status="running",
percent=shown_percent,
message=message,
)
return_code = process.wait()
if return_code == 0:
_set_youtube_job_state(
job_id,
status="completed",
percent=100.0,
message="Audio YouTube extrait et ajouté au stockage.",
)
return
detail = last_line if last_line else "Erreur inconnue"
_set_youtube_job_state(job_id, status="error", message=f"Échec extraction YouTube: {detail}")
def _trigger_sort_key(item: tuple[str, dict]) -> tuple[int, int | str]: def _trigger_sort_key(item: tuple[str, dict]) -> tuple[int, int | str]:
trigger_id = item[0] trigger_id = item[0]
if trigger_id.isdigit(): if trigger_id.isdigit():
@ -144,10 +336,12 @@ def dashboard() -> str | Response:
client = client_or_redirect client = client_or_redirect
triggers: dict[str, dict] = {} triggers: dict[str, dict] = {}
used_audio_files: set[str] = set()
try: try:
raw = client.list_triggers() raw = client.list_triggers()
valid_triggers = {k: v for k, v in raw.items() if isinstance(v, dict)} valid_triggers = {k: v for k, v in raw.items() if isinstance(v, dict)}
triggers = dict(sorted(valid_triggers.items(), key=_trigger_sort_key)) triggers = dict(sorted(valid_triggers.items(), key=_trigger_sort_key))
used_audio_files = _used_audio_files_from_triggers(valid_triggers)
except BackendApiError as exc: except BackendApiError as exc:
flash(f"Impossible de charger les triggers: {exc}", "error") flash(f"Impossible de charger les triggers: {exc}", "error")
@ -157,6 +351,7 @@ def dashboard() -> str | Response:
"dashboard.html", "dashboard.html",
triggers=triggers, triggers=triggers,
audio_files=audio_files, audio_files=audio_files,
used_audio_files=used_audio_files,
backend_url=session.get("backend_url", ""), backend_url=session.get("backend_url", ""),
username=session.get("backend_username", ""), username=session.get("backend_username", ""),
) )
@ -168,8 +363,16 @@ def audio_storage() -> str | Response:
if isinstance(client_or_redirect, Response): if isinstance(client_or_redirect, Response):
return client_or_redirect return client_or_redirect
client = client_or_redirect
audio_files = _list_audio_files() audio_files = _list_audio_files()
return render_template("audio_storage.html", audio_files=audio_files) used_audio_files: set[str] = set()
try:
raw = client.list_triggers()
valid_triggers = {k: v for k, v in raw.items() if isinstance(v, dict)}
used_audio_files = _used_audio_files_from_triggers(valid_triggers)
except BackendApiError as exc:
flash(f"Impossible de charger les triggers: {exc}", "error")
return render_template("audio_storage.html", audio_files=audio_files, used_audio_files=used_audio_files)
@ui.post("/trigger/save") @ui.post("/trigger/save")
@ -353,6 +556,29 @@ def upload_audio() -> Response:
if isinstance(client_or_redirect, Response): if isinstance(client_or_redirect, Response):
return client_or_redirect return client_or_redirect
youtube_url = request.form.get("youtube_url", "").strip()
if youtube_url:
requested_name = request.form.get("youtube_filename", "").strip()
command, command_error = _build_youtube_download_command(youtube_url, requested_name, _music_dir())
if command is None:
flash(command_error or "Impossible de préparer la commande yt-dlp.", "error")
return redirect(_audio_redirect_target())
try:
result = subprocess.run(command, check=False, capture_output=True, text=True)
except OSError as exc:
flash(f"Impossible d'exécuter yt-dlp: {exc}", "error")
return redirect(_audio_redirect_target())
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip().splitlines()
message = detail[-1] if detail else "Erreur inconnue"
flash(f"Échec extraction YouTube: {message}", "error")
return redirect(_audio_redirect_target())
flash("Audio YouTube extrait et ajouté au stockage.", "success")
return redirect(_audio_redirect_target())
audio = request.files.get("audio_file") audio = request.files.get("audio_file")
if audio is None or audio.filename is None or audio.filename.strip() == "": if audio is None or audio.filename is None or audio.filename.strip() == "":
flash("Sélectionnez d'abord un fichier.", "error") flash("Sélectionnez d'abord un fichier.", "error")
@ -395,11 +621,111 @@ def delete_audio() -> Response:
flash("Fichier introuvable.", "error") flash("Fichier introuvable.", "error")
return redirect(_audio_redirect_target()) return redirect(_audio_redirect_target())
client = client_or_redirect
try:
raw = client.list_triggers()
valid_triggers = {k: v for k, v in raw.items() if isinstance(v, dict)}
used_audio_files = _used_audio_files_from_triggers(valid_triggers)
if Path(filename).name.lower() in used_audio_files:
flash("Ce fichier est associé à un trigger et ne peut pas être supprimé.", "error")
return redirect(_audio_redirect_target())
except BackendApiError as exc:
flash(f"Impossible de vérifier les triggers: {exc}", "error")
return redirect(_audio_redirect_target())
target.unlink() target.unlink()
flash(f"Fichier {filename} supprimé.", "success") flash(f"Fichier {filename} supprimé.", "success")
return redirect(_audio_redirect_target()) return redirect(_audio_redirect_target())
@ui.post("/audio/youtube/proposed-name")
def youtube_proposed_name() -> Response:
client_or_redirect = _ensure_login()
if isinstance(client_or_redirect, Response):
return jsonify({"ok": False, "error": "Non connecte"}), 401
youtube_url = request.form.get("youtube_url", "").strip()
if youtube_url == "":
return jsonify({"ok": False, "error": "Lien YouTube manquant."}), 400
yt_dlp_path = shutil.which("yt-dlp")
if yt_dlp_path is None:
return jsonify({"ok": False, "error": "yt-dlp est introuvable sur le système."}), 400
if shutil.which("node") is None:
return jsonify({"ok": False, "error": "node est requis pour l'option --js-runtimes node."}), 400
command = [
yt_dlp_path,
"--js-runtimes",
"node",
"--remote-components",
"ejs:github",
"--no-playlist",
"--skip-download",
"--print",
"%(title)s",
youtube_url,
]
try:
result = subprocess.run(command, check=False, capture_output=True, text=True)
except OSError as exc:
return jsonify({"ok": False, "error": f"Impossible d'exécuter yt-dlp: {exc}"}), 500
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip().splitlines()
message = detail[-1] if detail else "Erreur inconnue"
return jsonify({"ok": False, "error": f"Échec lecture metadonnées YouTube: {message}"}), 502
title = next((line.strip() for line in result.stdout.splitlines() if line.strip()), "")
proposed = _normalize_youtube_filename(title) or "audio_youtube.mp3"
existing_names = {item.name.lower() for item in _music_dir().iterdir() if item.is_file()}
available_name = _next_available_filename(proposed, existing_names)
return jsonify({"ok": True, "filename": available_name})
@ui.post("/audio/youtube/start")
def youtube_download_start() -> Response:
client_or_redirect = _ensure_login()
if isinstance(client_or_redirect, Response):
return jsonify({"ok": False, "error": "Non connecte"}), 401
youtube_url = request.form.get("youtube_url", "").strip()
if youtube_url == "":
return jsonify({"ok": False, "error": "Lien YouTube manquant."}), 400
requested_name = request.form.get("youtube_filename", "").strip()
music_dir = _music_dir()
command, command_error = _build_youtube_download_command(youtube_url, requested_name, music_dir)
if command is None:
return jsonify({"ok": False, "error": command_error or "Impossible de préparer la commande yt-dlp."}), 400
job_id = _create_youtube_job()
worker = threading.Thread(target=_run_youtube_download_job, args=(job_id, command), daemon=True)
worker.start()
return jsonify({"ok": True, "job_id": job_id})
@ui.get("/audio/youtube/status/<job_id>")
def youtube_download_status(job_id: str) -> Response:
client_or_redirect = _ensure_login()
if isinstance(client_or_redirect, Response):
return jsonify({"ok": False, "error": "Non connecte"}), 401
with _YOUTUBE_JOBS_LOCK:
job = _YOUTUBE_JOBS.get(job_id)
if job is None:
return jsonify({"ok": False, "error": "Téléchargement introuvable."}), 404
payload = {
"ok": True,
"status": str(job.get("status", "running")),
"percent": float(job.get("percent", 0.0)),
"message": str(job.get("message", "Téléchargement en cours...")),
}
return jsonify(payload)
@ui.get("/audio/download/<path:filename>") @ui.get("/audio/download/<path:filename>")
def download_audio(filename: str) -> Response: def download_audio(filename: str) -> Response:
client_or_redirect = _ensure_login() client_or_redirect = _ensure_login()

288
frontend/app/templates/audio_storage.html

@ -19,12 +19,20 @@
<button type="submit">Téléverser</button> <button type="submit">Téléverser</button>
</form> </form>
<form method="post" action="{{ url_for('ui.upload_audio') }}" class="upload-row" id="upload-youtube-form" style="margin-top: 0.75rem;">
<input type="hidden" name="return_to" value="audio_storage" />
<input type="hidden" name="youtube_filename" id="youtube_filename_input" value="" />
<input type="url" name="youtube_url" id="youtube_url_input" placeholder="Lien YouTube (https://...)" required />
<button type="submit">Extraire via yt-dlp</button>
</form>
{% if audio_files %} {% if audio_files %}
<ul class="audio-list"> <ul class="audio-list">
{% for filename in audio_files %} {% for filename in audio_files %}
<li> <li>
<span>{{ filename }}</span> <span>{{ filename }}</span>
<div class="audio-actions"> <div class="audio-actions">
{% set is_used_by_trigger = filename|lower in used_audio_files %}
<button <button
type="button" type="button"
class="small-button js-play-browser" class="small-button js-play-browser"
@ -37,7 +45,11 @@
<form method="post" action="{{ url_for('ui.delete_audio') }}" class="inline-form js-delete-audio-form" data-filename="{{ filename }}"> <form method="post" action="{{ url_for('ui.delete_audio') }}" class="inline-form js-delete-audio-form" data-filename="{{ filename }}">
<input type="hidden" name="filename" value="{{ filename }}" /> <input type="hidden" name="filename" value="{{ filename }}" />
<input type="hidden" name="return_to" value="audio_storage" /> <input type="hidden" name="return_to" value="audio_storage" />
<button type="submit" class="small danger">Supprimer</button> <button
type="submit"
class="small danger"
{% if is_used_by_trigger %}disabled title="Impossible: fichier utilisé par un trigger"{% endif %}
>Supprimer</button>
</form> </form>
</div> </div>
</li> </li>
@ -59,6 +71,45 @@
</div> </div>
</div> </div>
<div class="modal-backdrop" id="youtube-rename-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="youtube-rename-title">
<h3 id="youtube-rename-title">Renommer avant téléchargement</h3>
<p id="youtube-rename-message">Vous pouvez modifier le nom proposé avant de lancer le téléchargement.</p>
<input type="text" id="youtube-rename-input" placeholder="nom_fichier.mp3" style="width: 100%; margin-top: 0.35rem;" />
<div class="modal-actions">
<button type="button" class="ghost-link" id="youtube-rename-cancel">Annuler</button>
<button type="button" id="youtube-rename-confirm">Télécharger</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="youtube-error-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="youtube-error-title">
<h3 id="youtube-error-title">Erreur import YouTube</h3>
<p id="youtube-error-message">Une erreur est survenue lors de la préparation du téléchargement.</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="youtube-error-close">Fermer</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="youtube-progress-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="youtube-progress-title">
<h3 id="youtube-progress-title">Téléchargement YouTube en cours</h3>
<div class="youtube-progress-wrap">
<div class="youtube-spinner" aria-hidden="true"></div>
<p id="youtube-progress-percent" class="youtube-progress-percent">0%</p>
</div>
<div class="youtube-progress-track" aria-hidden="true">
<div class="youtube-progress-bar" id="youtube-progress-bar"></div>
</div>
<p id="youtube-progress-message" class="muted">Préparation du téléchargement...</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="youtube-progress-close">Masquer</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="delete-audio-modal" aria-hidden="true"> <div class="modal-backdrop" id="delete-audio-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="delete-audio-title"> <div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="delete-audio-title">
<h3 id="delete-audio-title">Confirmer la suppression</h3> <h3 id="delete-audio-title">Confirmer la suppression</h3>
@ -85,6 +136,241 @@
</div> </div>
<script> <script>
(function () {
const form = document.getElementById("upload-youtube-form");
const youtubeUrlInput = document.getElementById("youtube_url_input");
const youtubeFilenameInput = document.getElementById("youtube_filename_input");
const modal = document.getElementById("youtube-rename-modal");
const renameInput = document.getElementById("youtube-rename-input");
const cancelBtn = document.getElementById("youtube-rename-cancel");
const confirmBtn = document.getElementById("youtube-rename-confirm");
const errorModal = document.getElementById("youtube-error-modal");
const errorMessage = document.getElementById("youtube-error-message");
const errorClose = document.getElementById("youtube-error-close");
const progressModal = document.getElementById("youtube-progress-modal");
const progressPercent = document.getElementById("youtube-progress-percent");
const progressBar = document.getElementById("youtube-progress-bar");
const progressMessage = document.getElementById("youtube-progress-message");
const progressClose = document.getElementById("youtube-progress-close");
if (!form || !youtubeUrlInput || !youtubeFilenameInput || !modal || !renameInput || !cancelBtn || !confirmBtn) return;
const submitButton = form.querySelector("button[type='submit']");
if (!submitButton) return;
let pollTimer = null;
let activeJobId = "";
const setLoading = (loading, label) => {
submitButton.disabled = loading;
submitButton.textContent = loading ? (label || "Analyse...") : "Extraire via yt-dlp";
};
const openModal = (filename) => {
renameInput.value = filename;
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
renameInput.focus();
renameInput.select();
};
const closeModal = () => {
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
const openErrorModal = (message) => {
if (!errorModal || !errorMessage || !errorClose) return;
errorMessage.textContent = message;
errorModal.classList.add("is-open");
errorModal.setAttribute("aria-hidden", "false");
errorClose.focus();
};
const closeErrorModal = () => {
if (!errorModal) return;
errorModal.classList.remove("is-open");
errorModal.setAttribute("aria-hidden", "true");
};
const openProgressModal = (percent, message) => {
if (!progressModal || !progressPercent || !progressBar || !progressMessage) return;
progressPercent.textContent = Math.max(0, Math.min(100, percent)).toFixed(1) + "%";
progressBar.style.width = Math.max(0, Math.min(100, percent)) + "%";
progressMessage.textContent = message || "Téléchargement en cours...";
progressModal.classList.add("is-open");
progressModal.setAttribute("aria-hidden", "false");
};
const closeProgressModal = () => {
if (!progressModal) return;
progressModal.classList.remove("is-open");
progressModal.setAttribute("aria-hidden", "true");
};
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
activeJobId = "";
};
const updateProgress = (percent, message) => {
if (!progressPercent || !progressBar || !progressMessage) return;
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
progressPercent.textContent = safePercent.toFixed(1) + "%";
progressBar.style.width = safePercent + "%";
progressMessage.textContent = message || "Téléchargement en cours...";
};
const statusUrlTemplate = "{{ url_for('ui.youtube_download_status', job_id='__JOB_ID__') }}";
const pollStatus = async () => {
if (!activeJobId) return;
try {
const response = await fetch(statusUrlTemplate.replace("__JOB_ID__", encodeURIComponent(activeJobId)), {
headers: { "X-Requested-With": "fetch" },
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
stopPolling();
setLoading(false);
closeProgressModal();
openErrorModal(payload.error || "Impossible de suivre le téléchargement.");
return;
}
updateProgress(payload.percent, payload.message);
if (payload.status === "completed") {
stopPolling();
setLoading(false);
updateProgress(100, payload.message || "Téléchargement terminé.");
setTimeout(() => {
window.location.reload();
}, 500);
return;
}
if (payload.status === "error") {
stopPolling();
setLoading(false);
closeProgressModal();
openErrorModal(payload.message || "Échec du téléchargement YouTube.");
}
} catch (_) {
stopPolling();
setLoading(false);
closeProgressModal();
openErrorModal("Erreur réseau pendant le suivi du téléchargement.");
}
};
const startYoutubeDownload = async (filename) => {
const youtubeUrl = (youtubeUrlInput.value || "").trim();
if (!youtubeUrl) return;
youtubeFilenameInput.value = filename;
setLoading(true, "Démarrage...");
openProgressModal(0, "Préparation du téléchargement...");
try {
const response = await fetch("{{ url_for('ui.youtube_download_start') }}", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Requested-With": "fetch",
},
body: new URLSearchParams({
youtube_url: youtubeUrl,
youtube_filename: filename,
}),
});
const payload = await response.json();
if (!response.ok || !payload.ok || !payload.job_id) {
setLoading(false);
closeProgressModal();
openErrorModal(payload.error || "Impossible de démarrer le téléchargement.");
return;
}
activeJobId = payload.job_id;
pollTimer = setInterval(pollStatus, 900);
pollStatus();
} catch (_) {
setLoading(false);
closeProgressModal();
openErrorModal("Erreur réseau lors du démarrage du téléchargement.");
}
};
form.addEventListener("submit", async (event) => {
event.preventDefault();
const youtubeUrl = (youtubeUrlInput.value || "").trim();
if (!youtubeUrl) return;
setLoading(true, "Analyse...");
try {
const response = await fetch("{{ url_for('ui.youtube_proposed_name') }}", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Requested-With": "fetch",
},
body: new URLSearchParams({ youtube_url: youtubeUrl }),
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
openErrorModal(payload.error || "Impossible de proposer un nom de fichier.");
return;
}
openModal(payload.filename || "audio_youtube.mp3");
} catch (_) {
openErrorModal("Erreur réseau lors de la récupération du nom proposé.");
} finally {
setLoading(false);
}
});
cancelBtn.addEventListener("click", closeModal);
confirmBtn.addEventListener("click", () => {
const filename = (renameInput.value || "").trim();
if (!filename) {
renameInput.focus();
return;
}
closeModal();
startYoutubeDownload(filename);
});
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
if (errorClose) {
errorClose.addEventListener("click", closeErrorModal);
}
if (errorModal) {
errorModal.addEventListener("click", (event) => {
if (event.target === errorModal) closeErrorModal();
});
}
if (progressClose) {
progressClose.addEventListener("click", closeProgressModal);
}
if (progressModal) {
progressModal.addEventListener("click", (event) => {
if (event.target === progressModal) closeProgressModal();
});
}
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal.classList.contains("is-open")) closeModal();
if (errorModal && event.key === "Escape" && errorModal.classList.contains("is-open")) closeErrorModal();
if (progressModal && event.key === "Escape" && progressModal.classList.contains("is-open")) closeProgressModal();
});
})();
(function () { (function () {
const player = document.getElementById("browser-audio-player"); const player = document.getElementById("browser-audio-player");
const timeLabel = document.getElementById("audio-time"); const timeLabel = document.getElementById("audio-time");

54
frontend/app/templates/base.html

@ -240,6 +240,13 @@ button:hover,
transform: translateY(-1px); transform: translateY(-1px);
} }
button:disabled,
.small-button:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
}
button.danger { button.danger {
background: var(--danger); background: var(--danger);
} }
@ -440,6 +447,53 @@ td {
gap: 0.6rem; gap: 0.6rem;
} }
.youtube-progress-wrap {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.youtube-spinner {
width: 22px;
height: 22px;
border-radius: 999px;
border: 3px solid rgba(15, 111, 99, 0.2);
border-top-color: var(--brand);
animation: spin 0.9s linear infinite;
flex-shrink: 0;
}
.youtube-progress-percent {
margin: 0;
font-weight: 700;
}
.youtube-progress-track {
margin-top: 0.65rem;
width: 100%;
height: 9px;
border-radius: 999px;
background: #ece7da;
overflow: hidden;
}
.youtube-progress-bar {
width: 0;
height: 100%;
background: linear-gradient(90deg, var(--brand), #1d8f7f);
transition: width 0.35s ease;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.audio-list { .audio-list {
list-style: none; list-style: none;
margin: 0; margin: 0;

1
frontend/requirements.txt

@ -1,3 +1,4 @@
Flask==3.0.3 Flask==3.0.3
requests==2.32.3 requests==2.32.3
gunicorn==23.0.0 gunicorn==23.0.0
yt-dlp==2026.3.17

Loading…
Cancel
Save