Twitter/X Media Downloader

Download videos/pictures with one click | Automatically package them into a ZIP file for batch download

  1. // ==UserScript==
  2. // @name Twitter/X Media Downloader
  3. // @name:af Twitter Media Download (2025.04.28 vas)
  4. // @name:am ትዊተር ሚዲያ አውርድ (2025.04.28 ተጠግኗል)
  5. // @name:ar تنزيل Media Twitter (2025.04.28 ثابت)
  6. // @name:az Twitter Media Yükləmə (2025.04.28 Sabit)
  7. // @name:be Загрузка Twitter Media (2025.04.28 Выпраўлена)
  8. // @name:bem Ukukongama kwa pa muulu (2025.04.28 ukwashintililwapo)
  9. // @name:bg Изтегляне на медии в Twitter (2025.04.28 Фиксиран)
  10. // @name:bn টুইটার মিডিয়া ডাউনলোড (2025.04.28 স্থির)
  11. // @name:bo Twitter Media Download (2025.04.28 གཏན་འཁེལ་བྱུང་བ།)
  12. // @name:bs Preuzimanje Twitter Media (2025.04.28 fiksnim)
  13. // @name:ca Descàrrega de Twitter Media (2025.04.28 S’ha corregit)
  14. // @name:ceb Download sa Twitter Media (2025.04.28 naayos)
  15. // @name:ckb داگرتنی میدیای تویتەر (2025.04.28 چاککراوە)
  16. // @name:cs Stahování médií Twitter (2025.04.28 Opraveno)
  17. // @name:cy Lawrlwytho Cyfryngau Twitter (2025.04.28 sefydlog)
  18. // @name:da Twitter Media Download (2025.04.28 Fixed)
  19. // @name:de Download von Twitter Media (2025.04.28 behoben)
  20. // @name:dv ޓްވިޓަރ މީޑިއާ ޑައުންލޯޑް (2025.04.28 ފިކްސްޑް)
  21. // @name:dz Twitter Medownload (2025.04.28 བདེ་སྒྲིག་)
  22. // @name:el Download Media Twitter (2025.04.28 Διορθώθηκε)
  23. // @name:en Twitter Media Downloader (2025.04.28 Fixed)
  24. // @name:en-GB Twitter Media Downloader (2025.04.28 Fixed)
  25. // @name:eo Elŝuti Twitter Media (2025.04.28 Fiksita)
  26. // @name:es Descarga de medios de Twitter (2025.04.28 solucionado)
  27. // @name:et Twitteri meedia allalaadimine (2025.04.28 fikseeritud)
  28. // @name:eu Twitter Media download (2025.04.28 Konpondu)
  29. // @name:fa دانلود رسانه توییتر (2025.04.28 ثابت)
  30. // @name:fi Twitter Media Download (2025.04.28 Kiinteä)
  31. // @name:fo Twitter Media Download (2025.04.28 Fast)
  32. // @name:fr Twitter Media Download (2025.04.28 Correction)
  33. // @name:fr-CA Twitter Media Download (2025.04.28 Correction)
  34. // @name:gd Luchdaich sìos meadhanan Twitter (2025.04.28 stèidhichte)
  35. // @name:gl Twitter Media Download (2025.04.28 Fixado)
  36. // @name:gu ટ્વિટર મીડિયા ડાઉનલોડ (2025.04.28 સ્થિર)
  37. // @name:haw Kāleʻa Twitter (2025.04.28 paʻa)
  38. // @name:he הורדת מדיה בטוויטר (2025.04.28 קבועה)
  39. // @name:hi ट्विटर मीडिया डाउनलोड (2025.04.28 फिक्स्ड)
  40. // @name:hr Twitter Media preuzimanje (2025.04.28 fiksno)
  41. // @name:ht Twitter Media Download (2025.04.28 fiks)
  42. // @name:hu A Twitter Media letöltése (2025.04.28 rögzített)
  43. // @name:hy Twitter Media Download (2025.04.28 ֆիքսված)
  44. // @name:id Unduh Media Twitter (2025.04.28 diperbaiki)
  45. // @name:is Twitter Media Download (2025.04.28 Fast)
  46. // @name:it Download di Twitter Media (2025.04.28 FISSO)
  47. // @name:ja Twitterメディアダウンロード(2025.04.28修正)
  48. // @name:ka Twitter Media Download (2025.04.28 დაფიქსირდა)
  49. // @name:kk Twitter Media Download (2025.04.28 тіркелген)
  50. // @name:km ការទាញយកប្រព័ន្ធផ្សព្វផ្សាយ Twitter (2025.04.28 ថេរ)
  51. // @name:kn ಟ್ವಿಟರ್ ಮಾಧ್ಯಮ ಡೌನ್‌ಲೋಡ್ (2025.04.28 ಸ್ಥಿರ)
  52. // @name:ko 트위터 미디어 다운로드 (2025.04.28 고정)
  53. // @name:ku Twitter Media Download (2025.04.28 Fixed)
  54. // @name:ky Twitter Media Download (2025.04.28 Fixed)
  55. // @name:la Twitter Media Download (2025.04.28 Fixarum)
  56. // @name:lb Twitter Medien eroflueden (2025.04.28 Fixéiert)
  57. // @name:lo Twitter media ດາວໂຫລດ (2025.04.28 ຄົງທີ່)
  58. // @name:lt „Twitter Media“ atsisiuntimas (fiksuota 2025.04.28)
  59. // @name:lv Twitter multivides lejupielāde (2025.04.28 fiksēts)
  60. // @name:mg Twitter Media Download (2025.04.28 Namboarina)
  61. // @name:mi Twitter Media Tango (2025.04.28 Kua whakaritea)
  62. // @name:mk Преземање на медиуми на Твитер (фиксно 2025.04.28)
  63. // @name:ml ട്വിറ്റർ മീഡിയ ഡൗൺലോഡ് (2025.04.28 സ്ഥിരമായി)
  64. // @name:mn Twitter Media татаж авах (2025.04.28 тогтмол)
  65. // @name:ms Twitter Media Muat turun (2025.04.28 Tetap)
  66. // @name:mt Twitter Media Download (2025.04.28 iffissat)
  67. // @name:my Twitter Media Download (2025.04.28)
  68. // @name:ne ट्विटर मिडिया डाउनलोड (20255.04.04.28 स्थिर)
  69. // @name:nl Twitter Media Download (2025.04.28 opgelost)
  70. // @name:no Twitter Media nedlasting (2025.04.28 Fast)
  71. // @name:ny Twitter Media Download (2025.04.28 Okhazikika)
  72. // @name:pa ਟਵਿੱਟਰ ਮੀਡੀਆ ਡਾ Download ਨਲੋਡ (2025.04.28 ਸਥਿਰ)
  73. // @name:pap Medida di Twitter Descarga (2025.04.28 Fiho)
  74. // @name:pl Pobieranie mediów na Twitterze (ustalone 2025.04.28)
  75. // @name:ps د ټویټر میډیا ډاونلوډ (2025.04.04.28 ټاکل شوی)
  76. // @name:pt Download de mídia do Twitter (2025.04.28 corrigido)
  77. // @name:pt-BR Download de mídia do Twitter (2025.04.28 corrigido)
  78. // @name:ro Descărcare media Twitter (2025.04.28 Fixată)
  79. // @name:ru Скачать Twitter Media (2025.04.28 Исправлена)
  80. // @name:rw Twitter Gukuramo (2025.04.28 Byagenwe)
  81. // @name:sg Twitter media Télécharger (2025.04.28 A leke ni)
  82. // @name:si ට්විටර් මාධ්ය බාගත කිරීම (2025.04.28 ස්ථාවර)
  83. // @name:sk Stiahnutie médií na Twitteri (2025.04.28 Opravené)
  84. // @name:sl Prenos medijev na Twitterju (2025.04.28 Fixed)
  85. // @name:sm Twitter Media Download (2025.04.28 Tumau)
  86. // @name:sn Twitter Media Download (2025.04.28 Yakagadziriswa)
  87. // @name:so Twitter Media Media Download (2025.04.28 go’an)
  88. // @name:sr Преузимање Твиттер Медиа (2025.04.28 фиксно)
  89. // @name:sv Twitter media nedladdning (2025.04.28 fast)
  90. // @name:sw Upakuaji wa media ya Twitter (2025.04.28 fasta)
  91. // @name:ta ட்விட்டர் மீடியா பதிவிறக்கம் (2025.04.28 சரி செய்யப்பட்டது)
  92. // @name:te ట్విట్టర్ మీడియా డౌన్‌లోడ్ (2025.04.28 పరిష్కరించబడింది)
  93. // @name:tg Twitter Media Download (2025.04.04.28 Стратегия)
  94. // @name:th ดาวน์โหลดสื่อ Twitter (2025.04.28 แก้ไข)
  95. // @name:ti ትዊተር ሚድያ ዳውንሎድ (2025.04.28 ጽኑዕ)
  96. // @name:tk Twitter Media göçürip almak (2025.04.28 kesgitlenen)
  97. // @name:tn Bobegakgang jwa Twitter Latalo (2025.04.28 E Tlhomamisitswe)
  98. // @name:to Twitter Mītia Download (2025.04.28 Tuʻunga)
  99. // @name:tpi Twitter Midia Daonlodem (2025.04.28 Oli fiksimap)
  100. // @name:tr Twitter Medya İndir (2025.04.28 Sabit)
  101. // @name:uk Завантажити медіа Twitter (2025.04.28 Виправлено)
  102. // @name:ur ٹویٹر میڈیا ڈاؤن لوڈ (2025.04.28 فکسڈ)
  103. // @name:uz Twitter Media Download (2025.04.28 belgilangan)
  104. // @name:vi Tải xuống phương tiện truyền thông Twitter (2025.04,28 đã sửa)
  105. // @name:xh I-Twitter Demiedia Eendaba (2025.04.28 ilungisiwe)
  106. // @name:yi טוויטטער מעדיע אראפקאפיע (2025.04.28 פאַרפעסטיקט)
  107. // @name:zh Twitter 媒体下载 (2025.04.28 修复)
  108. // @name:zh-CN Twitter 媒体下载 (2025.04.28 修复)
  109. // @name:zh-HK Twitter 媒體下載 (2025.04.28 修復)
  110. // @name:zh-MO Twitter 媒體下載 (2025.04.28 修復)
  111. // @name:zh-MY Twitter 媒体下载 (2025.04.28 修复)
  112. // @name:zh-SG Twitter 媒体下载 (2025.04.28 修复)
  113. // @name:zh-TW Twitter 媒體下載 (2025.04.28 修復)
  114. // @name:zu I-Twitter Media Download (2025.04.28 Ilungisiwe)
  115. // @description Download videos/pictures with one click | Automatically package them into a ZIP file for batch download
  116. // @description:af Laai video’s/prente met een klik af, en ondersteun outomatiese verpakking as ’n zip -lêer om af te laai wanneer bondel aflaai. Ondersteun nuwe API -koppelvlak
  117. // @description:am ቪዲዮዎችን / ስዕሎችን በአንድ ጠቅታ ያውርዱ እና የቡድን ማውረድ በሚረዱበት ጊዜ ለማውረድ ራስ-ሰር ማሸጊያዎችን እንደ ዚፕ ፋይል ይደግፉ. አዲስ የኤፒአይ በይነገጽን ይደግፉ
  118. // @description:ar قم بتنزيل مقاطع الفيديو/الصور بنقرة واحدة ، ودعم التغليف التلقائي كملف مضغوط للتنزيل عند تنزيل الدُفعات. دعم واجهة API جديدة
  119. // @description:az Videoları / şəkilləri bir kliklə yükləyin və toplu yükləmələri zamanı yükləmək üçün bir zip faylı olaraq avtomatik qablaşdırmanı dəstəkləyin. Yeni API interfeysini dəstəkləyin
  120. // @description:be Eénklik downloaden van video's/afbeeldingen, ondersteunt batch-download en pakt ze automatisch in een ZIP-bestand. Ondersteunt de nieuwste API-interface.
  121. // @description:bem Ukupoka amavidyo/amavidyo no kutinika fye kamo, no kwafwilisha ukupakasa ukwaibela nga failo ya ZIP iya kukopolola nga ca kuti ukukopolola kwa ciputulwa. Ukwafwilishako ifipya ifya API
  122. // @description:bg Изтеглете видеоклипове/снимки с едно щракване и поддържайте автоматична опаковка като Zip файл, за да изтеглите при изтегляне на партиди. Подкрепете нов API интерфейс
  123. // @description:bn এক ক্লিকের সাথে ভিডিও/ছবি ডাউনলোড করুন এবং ব্যাচ ডাউনলোড করার সময় ডাউনলোড করতে জিপ ফাইল হিসাবে স্বয়ংক্রিয় প্যাকেজিং সমর্থন করুন। নতুন এপিআই ইন্টারফেস সমর্থন করুন
  124. // @description:bo བརྙན་འཕྲིན་དང་པར་རིས་དེ་ཚོ་གཅིག་སྣུན་གཅིག་གིས་ཕབ་ལེན་བྱེད་པ་དང་། པར་རིས་ཕབ་ལེན་བྱེད་སྐབས་ཕབ་ལེན་བྱེད་པར་རང་འགུལ་གྱི་ཐུམ་སྒྲིལ་ལ་རྒྱབ་སྐྱོར་བྱེད་དགོས། API མཐུད་ཁ་གསར་པར་རྒྱབ་སྐྱོར་བྱེད།
  125. // @description:bs Preuzmite videozapise / slike jednim klikom i podržavajte automatsko pakovanje kao zip datoteku za preuzimanje kada se batch preuzima. Podržite novi API sučelje
  126. // @description:ca Descarregueu vídeos/imatges amb un clic i admeteu els envasos automàtics com a fitxer zip per descarregar -lo quan es descarreguen. Suporteu la nova interfície de l’API
  127. // @description:ceb I-download ang mga video / litrato nga adunay usa ka pag-klik, ug suportahan ang awtomatikong packaging ingon usa ka zip file aron ma-download kung ang mga pag-download sa batch. Pagsuporta sa bag-ong interface sa API
  128. // @description:ckb ڤیدیۆ/وێنەکان بە یەک کلیک دابەزێنە، و پشتگیری لە پاکەت و بەستەری ئۆتۆماتیکی بکە وەک فایلێکی زیپ بۆ داگرتن کاتێک وەجبە دابەزێنراوە. پشتگیری ڕووکاری نوێی API بکە
  129. // @description:cs Stáhněte si videa/obrázky s jedním kliknutím a podporujte automatické obaly jako soubor ZIP ke stažení při stahování dávek. Podporujte nové rozhraní API
  130. // @description:cy Dadlwythwch fideos/lluniau gydag un clic, a chefnogwch becynnu awtomatig fel ffeil zip i’w lawrlwytho wrth lawrlwythiadau swp. Cefnogwch ryngwyneb API newydd
  131. // @description:da Download videoer/billeder med et enkelt klik, og support automatisk emballage som en zip -fil, der skal downloades, når batch -downloads. Støtt nyt API -interface
  132. // @description:de Laden Sie Videos/Bilder mit einem Klick herunter und unterstützen Sie die automatische Verpackung als ZIP -Datei zum Herunterladen beim Stapel -Download. Unterstützen Sie die neue API -Schnittstelle
  133. // @description:dv އެއް ކްލިކުން ވީޑިއޯ/ތަސްވީރުތައް ޑައުންލޯޑްކޮށް، ބެޗް ޑައުންލޯޑް ކުރާއިރު ޑައުންލޯޑް ކުރުމަށް ޒިޕް ފައިލްއެއްގެ ގޮތުގައި އޮޓޮމެޓިކް ޕެކޭޖިންގ އަށް ސަޕޯޓް ކުރައްވާށެވެ. އާ އެޕީއައި އިންޓަފޭސް އަށް ސަޕޯޓް ދިނުން
  134. // @description:dz ཨེབ་གཏང་གཅིག་གིས་ བརྙན་འཕྲིན་/པར་རིས་ཚུ་ཕབ་ལེན་འབད་ཞིནམ་ལས་ བེཆ་ཕབ་ལེན་འབད་བའི་སྐབས་ ཕབ་ལེན་འབད་ནིའི་དོན་ལུ་ ཛིཔ་ཡིག་སྣོད་སྦེ་ རང་བཞིན་ཐུམ་སྒྲིལ་ལུ་རྒྱབ་སྐྱོར་འབད། ཨེ་པི་ཨའི་ ངོས་འདྲ་བ་གསརཔ་རྒྱབ་སྐྱོར་འབད།
  135. // @description:el Κατεβάστε βίντεο/εικόνες με ένα κλικ και υποστηρίξτε την αυτόματη συσκευασία ως αρχείο ZIP για λήψη όταν κατεβάσετε κατά τις λήψεις. Υποστήριξη νέας διασύνδεσης API
  136. // @description:en Download videos/pictures with one click, and support automatic packaging as a ZIP file to download when batch downloads. Support new API interface
  137. // @description:en-GB Download videos/pictures with one click, and support automatic packaging as a ZIP file to download when batch downloads. Support new API interface
  138. // @description:eo Elŝutu filmetojn/bildojn per unu alklako, kaj subtenu aŭtomatan pakaĵon kiel zip -dosieron por elŝuti kiam elŝutoj de batch. Subtenu novan API -interfacon
  139. // @description:es Descargue videos/imágenes con un solo clic y admite el embalaje automático como un archivo zip para descargar cuando se descarga por lotes. Admite una nueva interfaz API
  140. // @description:et Laadige alla videod/pildid ühe klõpsuga ja toetage automaatset pakendit ZIP -failina, mida partii allalaadimisel alla laadida. Toetage uut API -liidest
  141. // @description:eu Deskargatu bideoak / irudiak klik bakarrarekin eta laguntza automatikoko pakete gisa deskargatzeko, lote deskargatzen direnean deskargatzeko. Laguntza API interfaze berria
  142. // @description:fa فیلم ها/تصاویر را با یک کلیک بارگیری کنید و از بسته بندی های خودکار به عنوان یک فایل zip پشتیبانی کنید تا هنگام بارگیری دسته ای بارگیری شود. از رابط جدید API پشتیبانی کنید
  143. // @description:fi Lataa videoita/kuvia yhdellä napsautuksella ja tue automaattista pakkausta zip -tiedostona ladattaessa erän latauksia. Tukea uutta API -käyttöliittymää
  144. // @description:fo Heinta video/myndir við einum klikki, og stuðla sjálvvirkandi pakking sum ZIP fílu at heinta tá batch downloads. Stuðla nýggjum API-grunnflati
  145. // @description:fr Téléchargez des vidéos / photos en un clic et prends en charge l’emballage automatique comme un fichier zip à télécharger lors des téléchargements par lots. Soutenez la nouvelle interface API
  146. // @description:fr-CA Téléchargez des vidéos / photos en un clic et prends en charge l’emballage automatique comme un fichier zip à télécharger lors des téléchargements par lots. Soutenez la nouvelle interface API
  147. // @description:gd Luchdaich sìos bhideothan / dealbhan le aon bhriogadh, agus a ’toirt taic do phostadh fèin-ghluasadach mar fhaidhle zip ri luchdachadh sìos nuair a chleachd thu gu do luchdachadh sìos bailte. Cuir taic ri eadar-aghaidh API ùr
  148. // @description:gl Descarga vídeos/imaxes cun só clic e admite os envases automáticos como ficheiro zip para descargar cando as descargas por lotes. Apoia a nova interface API
  149. // @description:gu એક ક્લિક સાથે વિડિઓઝ/ચિત્રો ડાઉનલોડ કરો અને જ્યારે બેચ ડાઉનલોડ્સ ડાઉનલોડ કરવા માટે ઝિપ ફાઇલ તરીકે સ્વચાલિત પેકેજિંગને સપોર્ટ કરો. નવા API ઇન્ટરફેસને સપોર્ટ કરો
  150. // @description:haw Hoʻoiho i nā wikiō / nā kiʻi me kahi kaomi hoʻokahi, a kākoʻo i ka paleʻana ma keʻano he file zip e hoʻoiho i ka wā e hoʻoiho ai nā waihona. Kākoʻo Ui Openta Interneface
  151. // @description:he הורד סרטונים/תמונות בלחיצה אחת, ותמך באריזה אוטומטית כקובץ מיקוד כדי להוריד כאשר הורדות אצווה. תמיכה בממשק API חדש
  152. // @description:hi एक क्लिक के साथ वीडियो/चित्र डाउनलोड करें, और बैच डाउनलोड होने पर डाउनलोड करने के लिए एक ज़िप फ़ाइल के रूप में स्वचालित पैकेजिंग का समर्थन करें। नए एपीआई इंटरफ़ेस का समर्थन करें
  153. // @description:hr Preuzmite videozapise/slike jednim klikom i podržavajte automatsko pakiranje kao zip datoteku za preuzimanje kada se preuzmu batch. Podržite novo API sučelje
  154. // @description:ht Download videyo/foto ak yon sèl klike sou, ak sipòte anbalaj otomatik kòm yon dosye postal yo download lè pakèt downloads. Sipòte nouvo koòdone API
  155. // @description:hu Töltse le a videókat/képeket egy kattintással, és támogassa az automatikus csomagolást ZIP fájlként, hogy letöltse a tétel letöltésekor. Támogassa az új API felületet
  156. // @description:hy Ներբեռնեք տեսանյութեր / նկարներ մեկ կտտոցով եւ աջակցեք ավտոմատ փաթեթավորմանը `որպես փոստային ֆայլ ներբեռնելու համար, երբ ներլցվում է խմբաքանակի ներլցումներ: Աջակցեք նոր API ինտերֆեյսին
  157. // @description:id Unduh video/gambar dengan satu klik, dan mendukung kemasan otomatis sebagai file zip untuk diunduh saat unduhan batch. Dukung Antarmuka API Baru
  158. // @description:is Sæktu myndbönd/myndir með einum smelli og styððu sjálfvirkar umbúðir sem zip skrá til að hlaða niður þegar lotu niðurhal. Styðjið nýtt API viðmót
  159. // @description:it Scarica video/immagini con un clic e supporta l’imballaggio automatico come file zip da scaricare quando i download batch. Supportare la nuova interfaccia API
  160. // @description:ja ワンクリックでビデオ/写真をダウンロードし、バッチダウンロード時にダウンロードするzipファイルとして自動パッケージをサポートします。新しいAPIインターフェイスをサポートします
  161. // @description:ka ჩამოტვირთეთ ვიდეო/სურათები ერთი დაწკაპუნებით და მხარი დაუჭირეთ ავტომატურ შეფუთვას, როგორც ZIP ფაილი, რომ ჩამოტვირთოთ ჯგუფების ჩამოტვირთვისას. ახალი API ინტერფეისის მხარდაჭერა
  162. // @description:kk Бейнелерді / суреттерді бір рет нұқыңыз және ZIP файлы ретінде автоматты түрде орауышпен жүктеп алыңыз. Жаңа API интерфейсін қолдаңыз
  163. // @description:km ទាញយកវីដេអូ / រូបភាពដោយចុចតែម្តងចុចការវេចខ្ចប់ដោយស្វ័យប្រវត្តិជាឯកសារហ្ស៊ីបដើម្បីទាញយកនៅពេលទាញយកបាច់។ គាំទ្រចំណុចប្រទាក់ API ថ្មី
  164. // @description:kn ಒಂದು ಕ್ಲಿಕ್‌ನೊಂದಿಗೆ ವೀಡಿಯೊಗಳು/ಚಿತ್ರಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ, ಮತ್ತು ಬ್ಯಾಚ್ ಡೌನ್‌ಲೋಡ್‌ಗಳು ಯಾವಾಗ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಜಿಪ್ ಫೈಲ್ ಆಗಿ ಸ್ವಯಂಚಾಲಿತ ಪ್ಯಾಕೇಜಿಂಗ್ ಅನ್ನು ಬೆಂಬಲಿಸಿ. ಹೊಸ API ಇಂಟರ್ಫೇಸ್ ಅನ್ನು ಬೆಂಬಲಿಸಿ
  165. // @description:ko 한 번의 클릭으로 비디오/사진을 다운로드하고 배치 다운로드 할 때 다운로드 할 수있는 zip 파일로 자동 포장을 지원하십시오. 새로운 API 인터페이스를 지원합니다
  166. // @description:ku Vîdyoyên / wêneyên bi yek klîk dakêşin, û piştgiriyê bidin pakkirina otomatîk wekî pelek zip ku dakêşin dema daxistinên batch. Piştgiriya API ya nû piştgirî bikin
  167. // @description:ky Видеолорду / сүрөттөрдү бир чыкылдатуу менен жүктөп алыңыз жана партия жүктөлүп жүктөөлөрдү жүктөп алуу үчүн автоматтык түрдө таңгактоону жүктөп алыңыз. Жаңы API интерфейсинти колдоо
  168. // @description:la Download Videos / Pictures una cum una click, et firmamentum automatic packaging ut ZIP lima ut download cum batch downloads. Support New API interface
  169. // @description:lb تحميل الفيديوهات/الصور بنقرة واحدة، يدعم التحميل الجماعي وتعبئتها تلقائيًا في ملف ZIP. يدعم واجهة API الجديدة.
  170. // @description:lo ດາວໂຫລດວິດີໂອ / ຮູບພາບດ້ວຍການກົດປຸ່ມດຽວ, ແລະສະຫນັບສະຫນູນການຫຸ້ມຫໍ່ແບບອັດຕະໂນມັດເປັນແຟ້ມ ZIP ເພື່ອດາວໂຫລດໃນເວລາທີ່ດາວໂຫລດມາ. ສະຫນັບສະຫນູນການໂຕ້ຕອບ API ໃຫມ່
  171. // @description:lt Atsisiųskite vaizdo įrašus/paveikslėlius vienu paspaudimu ir palaikykite automatines pakuotes kaip ZIP failą, kurį galite atsisiųsti, kai atsisiunčiami partijos. Palaikykite naują API sąsają
  172. // @description:lv Lejupielādējiet videoklipus/attēlus ar vienu klikšķi un atbalstiet automātisko iesaiņojumu kā zip failu, lai lejupielādētu, kad parasti lejupielādē. Atbalstiet jauno API saskarni
  173. // @description:mg Misintona horonantsary / sary miaraka amin’ny tsindry iray, ary manohana fonosana mandeha ho azy amin’ny rakitra zip mba hisintona rehefa download batch. Tohanana ny interface voalohany API
  174. // @description:mi Tangohia nga ataata / pikitia me te paato, me te tautoko i te kapi aunoa hei konae kao hei tango i te wa o nga hapai. Tautokohia te atanga API hou
  175. // @description:mk Преземете видеа/слики со еден клик, и поддржувајте автоматско пакување како поштенска датотека за преземање кога преземате серии. Поддржете нов интерфејс API
  176. // @description:ml ഒരു ക്ലിക്കിലൂടെ വീഡിയോകൾ / ചിത്രങ്ങൾ ഡൗൺലോഡുചെയ്യുക, ബാച്ച് ഡൗൺലോഡുകൾ ഡ download ൺലോഡ് ചെയ്യാനുള്ള ഒരു സിപ്പ് ഫയലായി യാന്ത്രിക പാക്കേജിംഗിനെ പിന്തുണയ്ക്കുക. പുതിയ API ഇന്റർഫേസിനെ പിന്തുണയ്ക്കുക
  177. // @description:mn Багцыг нэг товшилтоор татаж авах, автомат багц татаж авах, багц татаж авахад автомат багцыг татаж авах. Шинэ API интерфэйсийг дэмжинэ
  178. // @description:ms Muat turun video/gambar dengan satu klik, dan menyokong pembungkusan automatik sebagai fail zip untuk dimuat turun apabila muat turun batch. Sokong antara muka API baru
  179. // @description:mt Niżżel vidjows / stampi bi klikk waħda, u appoġġ l-imballaġġ awtomatiku bħala fajl zip biex tniżżel meta tniżżel lott. Appoġġ Interface API Ġdid
  180. // @description:my ဗွီဒီယိုများ / ရုပ်ပုံများကိုတစ်ချက်နှိပ်ပြီး batch downloads ကို download လုပ်သည့်အခါအလိုအလျောက်ထုပ်ပိုးမှုကို Automatic Packaging ကိုကူညီပါ။ API interface အသစ်ကိုထောက်ပံ့ပါ
  181. // @description:ne भिडियोहरू / चित्रहरू डाउनलोड गर्नुहोस् एक क्लिकमा क्लिक गर्नुहोस्, र डाउनलोड डाउनलोड गर्न एक क्लिक फाइलमा स्वचालित प्याकेजिंगलाई समर्थन गर्नुहोस्। नयाँ api ईन्टरफेस समर्थन गर्नुहोस्
  182. // @description:nl Download video’s/foto’s met één klik en ondersteun automatische verpakkingen als een zip -bestand om te downloaden wanneer batch downloads. Steun nieuwe API -interface
  183. // @description:no Last ned videoer/bilder med ett klikk, og støtt automatisk emballasje som en zip -fil for å laste ned når batch nedlastinger. Støtt nytt API -grensesnitt
  184. // @description:ny Tsitsani mavidiyo / zithunzi ndi dinani imodzi, ndikuchirikiza makina okhawo ngati fayilo ya zip kuti mutsitse pomwe ma batch otsitsa. Thandizani API yatsopano ya API
  185. // @description:pa ਇਕ ਕਲਿਕ ਦੇ ਨਾਲ ਵੀਡੀਓ / ਤਸਵੀਰਾਂ ਡਾ Download ਨਲੋਡ ਕਰੋ, ਅਤੇ ਬੈਚ ਡਾਉਨਲੋਡ ਕਰਨ ਲਈ ਇੱਕ ਕਲਿਕ ਕਰੋ. ਨਵੇਂ ਏਪੀਆਈ ਇੰਟਰਫੇਸ ਦਾ ਸਮਰਥਨ ਕਰੋ
  186. // @description:pap Download videonan/potretnan ku un klik, i sostené empaketamentu outomatiko komo un archivo ZIP pa download ora di downloads di lote. Sostené interfase nobo di API
  187. // @description:pl Pobierz filmy/zdjęcia za pomocą jednego kliknięcia i obsługuj automatyczne opakowanie jako plik zip, aby pobrać po pobraniu partii. Obsługuj nowy interfejs API
  188. // @description:ps د ویډیوګانو سره ویډیوګانې / عکسونه ډاونلوډ کړئ، او د ډاونلوډ فایل په توګه د زنايي بسته بندۍ په توګه د ډاونلوډ لپاره د زوم فایل په توګه ملاتړ وکړئ کله چې د بچ کښته کوونې. د نوي اپی انٹرفیس ملاتړ وکړئ
  189. // @description:pt Faça o download de vídeos/imagens com um clique e suporta a embalagem automática como um arquivo zip para download quando downloads em lote. Apoie a nova interface da API
  190. // @description:pt-BR Faça o download de vídeos/imagens com um clique e suporta a embalagem automática como um arquivo zip para download quando downloads em lote. Apoie a nova interface da API
  191. // @description:ro Descărcați videoclipuri/imagini cu un singur clic și acceptați ambalajele automate ca fișier zip pentru a descărca atunci când descărcări de lot. Susțineți interfața API nouă
  192. // @description:ru Загрузите видео/картинки с одним щелчком, и поддержать автоматическую упаковку в качестве zip -файла для загрузки при загрузке пакетов. Поддержать новый интерфейс API
  193. // @description:rw Kuramo amashusho / amashusho hamwe na kanda imwe, kandi ushyigikire gupakira byikora nka zip dosiye kugirango ukuremo mugihe bakuramo amashusho. Shigikira Imigaragarire Nshya ya API
  194. // @description:sg Téléchargé avidéo/afoto ni na lege ti mbeni clic oko, nga mû maboko na automatique tongana mbeni fichier ZIP ti téléchargé ni tongana a téléchargé ni. Mû maboko na fini interface ti API .
  195. // @description:si එක් ක්ලික් කිරීමකින් වීඩියෝ / පින්තූර බාගත කිරීම සමඟ බාගන්න, සහ ස්වයංක්රීය ඇසුරුම්කරණයට බාගත කිරීමේදී බාගත කිරීම සඳහා zip ගොනුවක් ලෙස ස්වයංක්රීය ඇසුරුම්කරණය කරන්න. නව API අතුරුමුහුණතට සහාය වන්න
  196. // @description:sk Stiahnite si videá/obrázky jedným kliknutím a podporujte automatické obaly ako súbor zip a stiahnite si pri stiahnutí dávky. Podporte nové rozhranie API
  197. // @description:sl Prenesite videoposnetke/slike z enim klikom in podprite samodejno embalažo kot zip datoteko, ki jo lahko prenesete, ko se prenašajo s paketom. Podprite nov vmesnik API
  198. // @description:sm Sii mai Vitio / ata ma le tasi kiliki, ma lagolago otometi afifiina o se faila faila e download pe a download downloads. Lagolagoina fou API interface
  199. // @description:sn Dhawunirodha mifananidzo / mifananidzo ine imwe chete tinya, uye rutsigiro otomatiki caping seye zip faira yekurodha kana batch downloads. Tsigira New API Interface
  200. // @description:so Kalasoco fiidiyowyada / sawirrada hal guji, oo taageer baakadaha otomatiga ah sida faylka zip si aad u soo dejiso markii dufcaddu soo dejiso. Taageerso Interface-ka cusub ee API
  201. // @description:sr Преузмите видео записе / слике једним кликом и подржавате аутоматско паковање као ЗИП датотеку за преузимање када се серишите. Подржите нови АПИ интерфејс
  202. // @description:sv Ladda ner videor/bilder med ett klick och stödja automatisk förpackning som zip -fil för att ladda ner när Batch Downloads. Stöd Nytt API -gränssnitt
  203. // @description:sw Pakua video/picha na bonyeza moja, na usaidie ufungaji wa moja kwa moja kama faili ya zip ili kupakua wakati wa kupakua. Kusaidia interface mpya ya API
  204. // @description:ta ஒரே கிளிக்கில் வீடியோக்கள்/படங்களை பதிவிறக்கம் செய்து, தொகுதி பதிவிறக்கும்போது பதிவிறக்குவதற்கு தானியங்கி பேக்கேஜிங்கை ஜிப் கோப்பாக ஆதரிக்கவும். புதிய ஏபிஐ இடைமுகத்தை ஆதரிக்கவும்
  205. // @description:te వీడియోలు/చిత్రాలను ఒకే క్లిక్‌తో డౌన్‌లోడ్ చేయండి మరియు బ్యాచ్ డౌన్‌లోడ్ చేసినప్పుడు డౌన్‌లోడ్ చేయడానికి ఆటోమేటిక్ ప్యాకేజింగ్‌కు జిప్ ఫైల్‌గా మద్దతు ఇవ్వండి. కొత్త API ఇంటర్‌ఫేస్‌కు మద్దతు ఇవ్వండి
  206. // @description:tg Видео / расмҳоро бо як клик зеркашӣ кунед ва бастабандӣ автоматикуниро ҳамчун файли ZIP барои зеркашӣ зеркашӣ кунед, вақте ки Боргирии гурӯҳҳо. Дастгирии интерфейси нави API
  207. // @description:th ดาวน์โหลดวิดีโอ/รูปภาพด้วยคลิกเดียวและสนับสนุนบรรจุภัณฑ์อัตโนมัติเป็นไฟล์ซิปเพื่อดาวน์โหลดเมื่อดาวน์โหลดแบทช์ รองรับอินเตอร์เฟส API ใหม่
  208. // @description:ti ቪድዮታት/ስእልታት ብሓደ ጠውቂ ኣውርድ፣ ከምኡ’ውን Batch Downloads ምስ ዝወርድ ከተውርድዎ እትኽእል ኣውቶማቲክ መዐሸጊ ከም ዚፕ ፋይል ደግፉ። ሓድሽ ኤፒኣይ ኢንተርፌስ ምድጋፍ
  209. // @description:tk Bir gezek basany / suratlary göçürip alyň we pol ýüklenende göçürip almak üçin poçta faýly göçürip alyň. Täze API interfeýseri goldamak
  210. // @description:tn Laisolola dibidio/ditshwantsho ka go tobetsa gangwe fela, mme o tshegetse go phuthela ka go itirisa jaaka faele ya ZIP go e laisolola fa di-download tsa ditlhopha. Tshegetsa segokaganyi se sešwa sa API
  211. // @description:to Download ’a e ngaahi vitio/ngaahi fakatata ’aki ha kiliki ’e taha, pea poupou’i ’a e ’otometiki ’a e packaging ko ha faile ZIP ke download ’i he taimi ’oku download ai ’a e batch. Poupou’i ’a e interface fo’ou API .
  212. // @description:tpi Daonlodem ol vidio/pikja wetem wan klik, mo sapotem otomatik pakejing olsem wan ZIP fael blong daonlodem taem yu stap daon long ol daon daonlod. Sapotim nupela API intafes
  213. // @description:tr Videoları/resimleri tek bir tıklamayla indirin ve toplu indirmeler yaparken indirilecek bir zip dosyası olarak otomatik ambalajı destekleyin. Yeni API arayüzünü destekleyin
  214. // @description:uk Завантажте відео/зображення одним клацанням та підтримайте автоматичну упаковку як Zip -файл для завантаження при завантаженні пакетів. Підтримуйте новий інтерфейс API
  215. // @description:ur ایک کلک کے ساتھ ویڈیوز/تصاویر ڈاؤن لوڈ کریں ، اور بیچ ڈاؤن لوڈ ہونے پر ڈاؤن لوڈ کرنے کے لئے خودکار پیکیجنگ کو زپ فائل کے طور پر سپورٹ کریں۔ نئے API انٹرفیس کی حمایت کریں
  216. // @description:uz Videolarni / rasmlarni bitta bosish bilan yuklab oling va partiya yuklab olinganda yuklab olish uchun ZIP fayl sifatida avtomatik qadoqlash. Yangi API interfeysini qo’llab-quvvatlang
  217. // @description:vi Tải xuống video/hình ảnh chỉ bằng một cú nhấp chuột và hỗ trợ bao bì tự động dưới dạng tệp zip để tải xuống khi tải xuống hàng loạt. Hỗ trợ giao diện API mới
  218. // @description:xh Khuphela iividiyo / imifanekiso kunye Cofa, kwaye uxhase ukupakishwa ngokuzenzekelayo njengefayile ye-zip ukuze ukhuphele xa kutsala umdla. INKXASO YOKUGQIBELA I-API
  219. // @description:yi אראפקאפיע ווידיאס / בילדער מיט איין גיט, און שטיצן אָטאַמאַטיק פּאַקקאַגינג ווי אַ פאַרשלעסלען טעקע צו אָפּלאָדירן ווען פּעקל דאַונלאָודז. סופּפּאָרט ניו אַפּי צובינד
  220. // @description:zh 一键下载视频/图片,支持批量下载时自动打包为一个ZIP文件下载.支持新版API接口
  221. // @description:zh-CN 一键下载视频/图片,支持批量下载时自动打包为一个ZIP文件下载.支持新版API接口
  222. // @description:zh-HK 一鍵下載視頻/圖片,支持批量下載時自動打包為一個ZIP文件下載.支持新版API接口
  223. // @description:zh-MO 一鍵下載視頻/圖片,支持批量下載時自動打包為一個ZIP文件下載.支持新版API接口
  224. // @description:zh-MY 一键下载视频/图片,支持批量下载时自动打包为一个ZIP文件下载.支持新版API接口
  225. // @description:zh-SG 一键下载视频/图片,支持批量下载时自动打包为一个ZIP文件下载.支持新版API接口
  226. // @description:zh-TW 一鍵下載視頻/圖片,支持批量下載時自動打包為一個ZIP文件下載.支持新版API接口
  227. // @description:zu Landa amavidiyo / izithombe ngokuchofoza okukodwa, bese usekela ukupakishwa okuzenzakalelayo njengefayela le-zip ukulanda lapho i-batch ilanda. Sekela interface entsha ye-API
  228. // @author goemon2017,天音,Tiande,人民的勤务员 <china.qinwuyuan@gmail.com>
  229. // @namespace https://github.com/ChinaGodMan/UserScripts
  230. // @supportURL https://github.com/ChinaGodMan/UserScripts/issues
  231. // @homepageURL https://github.com/ChinaGodMan/UserScripts
  232. // @license MIT
  233. // @icon https://raw.githubusercontent.com/ChinaGodMan/UserScriptsHistory/main/scriptsIcon/x.svg
  234. // @compatible chrome
  235. // @compatible firefox
  236. // @compatible edge
  237. // @compatible opera
  238. // @compatible safari
  239. // @compatible kiwi
  240. // @compatible qq
  241. // @compatible via
  242. // @compatible brave
  243. // @grant GM_registerMenuCommand
  244. // @grant GM_setValue
  245. // @grant GM_getValue
  246. // @grant GM_download
  247. // @match https://x.com/*
  248. // @match https://twitter.com/*
  249. // @version 2025.04.28.1719
  250. // @created 2025-03-11 08:11:29
  251. // @modified 2025-03-11 08:11:29
  252. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  253. // ==/UserScript==
  254. /**
  255. * File: twitter-media-downloader.user.js
  256. * Project: UserScripts
  257. * File Created: 2025/03/11,Tuesday 08:11:41
  258. * Author: goemon2017,天音,Tiande,人民的勤务员@ChinaGodMan (china.qinwuyuan@gmail.com)
  259. * -----
  260. * Last Modified: 2025/04/28,Monday 17:19:11
  261. * Last Modified: 2025/04/28,Monday 17:19:11
  262. * Modified By: 人民的勤务员@ChinaGodMan (china.qinwuyuan@gmail.com)
  263. * License: MIT License
  264. * Copyright © 2024 - 2025 ChinaGodMan,Inc
  265. */
  266.  
  267. //! 修复代码来自:goemon2017:https://greatest.deepsurf.us/scripts/423001/discussions/296626#comment-589742
  268.  
  269. /* jshint esversion: 8 */
  270. const filename = 'twitter_{user-name}(@{user-id})_{date-time}_{status-id}_{file-type}'
  271. const TMD = (function () {
  272. let lang, host, history, show_sensitive, is_tweetdeck
  273. return {
  274. init: async function () {
  275. GM_registerMenuCommand((this.language[navigator.language] || this.language.en).settings, this.settings)
  276. GM_registerMenuCommand('Export History (Markdown)', async () => this.exportHistory())
  277. lang = this.language[document.querySelector('html').lang] || this.language.en
  278. host = location.hostname
  279. is_tweetdeck = host.indexOf('tweetdeck') >= 0
  280. history = this.storage_obsolete()
  281. if (history.length) {
  282. this.storage(history)
  283. this.storage_obsolete(true)
  284. } else history = await this.storage()
  285. show_sensitive = GM_getValue('show_sensitive', false)
  286. document.head.insertAdjacentHTML('beforeend', '<style>' + this.css + (show_sensitive ? this.css_ss : '') + '</style>')
  287. let observer = new MutationObserver(ms => ms.forEach(m => m.addedNodes.forEach(node => this.detect(node))))
  288. observer.observe(document.body, { childList: true, subtree: true })
  289. },
  290. exportHistory: async function () {
  291. try {
  292. const history = await GM_getValue('download_history', [])
  293. if (!history || !Array.isArray(history) || history.length === 0) {
  294. return
  295. }
  296. const markdownContent = '# Twitter/X Media Downloader history\n\n' +
  297. (await Promise.all(history.map(id => this.generateMarkdown(id)))).join('\n')
  298. const blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' })
  299. const link = document.createElement('a')
  300. link.href = URL.createObjectURL(blob)
  301. link.download = `twitter_download_history_(${history.length}).md`
  302. document.body.appendChild(link)
  303. link.click()
  304. document.body.removeChild(link)
  305. URL.revokeObjectURL(link.href)
  306. } catch (error) {
  307. console.error('An error occurred while exporting Markdown history:', error)
  308. alert('An error occurred while exporting Markdown history, please check the console for details.')
  309. }
  310. },
  311. generateMarkdown: async function (tweet_id, fetch = true) {
  312. if (!fetch) return `[Tweet] - ${tweet_id} (https://x.com/i/web/status/${tweet_id})`
  313. let json = await this.fetchJson(tweet_id)
  314. let tweet = json.quoted_status_result?.result?.legacy?.media
  315. || json.quoted_status_result?.result?.legacy
  316. || json.legacy
  317. let user = json.core.user_results.result.legacy
  318. let user_name = user.name.replace(/([\\/|*?:"\u200b-\u200d\u2060\ufeff]|🔞)/g, v => invalid_chars[v])
  319. let full_text = tweet.full_text.split('\n').join(' ').replace(/\s*https:\/\/t\.co\/\w+/g, '').replace(/[\\/|<>*?:"\u200b-\u200d\u2060\ufeff]/g, v => invalid_chars[v])
  320. return `[${user_name} (@${user.screen_name})](https://x.com/i/web/status/${tweet_id})\n> ${full_text}\n`
  321. },
  322. detect: function (node) {
  323. let article = node.tagName == 'ARTICLE' && node || node.tagName == 'DIV' && (node.querySelector('article') || node.closest('article'))
  324. if (article) this.addButtonTo(article)
  325. let listitems = node.tagName == 'LI' && node.getAttribute('role') == 'listitem' && [node] || node.tagName == 'DIV' && node.querySelectorAll('li[role="listitem"]')
  326. if (listitems) this.addButtonToMedia(listitems)
  327. },
  328. addButtonTo: function (article) {
  329. if (article.dataset.detected) return
  330. article.dataset.detected = 'true'
  331. let media_selector = [
  332. 'a[href*="/photo/1"]',
  333. 'div[role="progressbar"]',
  334. 'button[data-testid="playButton"]',
  335. 'a[href="/settings/content_you_see"]', //hidden content
  336. 'div.media-image-container', // for tweetdeck
  337. 'div.media-preview-container', // for tweetdeck
  338. 'div[aria-labelledby]>div:first-child>div[role="button"][tabindex="0"]' //for audio (experimental)
  339. ]
  340. let media = article.querySelector(media_selector.join(','))
  341. if (media) {
  342. let status_id = article.querySelector('a[href*="/status/"]').href.split('/status/').pop().split('/').shift()
  343. let btn_group = article.querySelector('div[role="group"]:last-of-type, ul.tweet-actions, ul.tweet-detail-actions')
  344. let btn_share = Array.from(btn_group.querySelectorAll(':scope>div>div, li.tweet-action-item>a, li.tweet-detail-action-item>a')).pop().parentNode
  345. let btn_down = btn_share.cloneNode(true)
  346. btn_down.querySelector('button').removeAttribute('disabled')
  347. if (is_tweetdeck) {
  348. btn_down.firstElementChild.innerHTML = '<svg viewBox="0 0 24 24" style="width: 18px; height: 18px;">' + this.svg + '</svg>'
  349. btn_down.firstElementChild.removeAttribute('rel')
  350. btn_down.classList.replace('pull-left', 'pull-right')
  351. } else {
  352. btn_down.querySelector('svg').innerHTML = this.svg
  353. }
  354. let is_exist = history.indexOf(status_id) >= 0
  355. this.status(btn_down, 'tmd-down')
  356. this.status(btn_down, is_exist ? 'completed' : 'download', is_exist ? lang.completed : lang.download)
  357. btn_group.insertBefore(btn_down, btn_share.nextSibling)
  358. btn_down.onclick = () => this.click(btn_down, status_id, is_exist)
  359. if (show_sensitive) {
  360. let btn_show = article.querySelector('div[aria-labelledby] div[role="button"][tabindex="0"]:not([data-testid]) > div[dir] > span > span')
  361. if (btn_show) btn_show.click()
  362. }
  363. }
  364. let imgs = article.querySelectorAll('a[href*="/photo/"]')
  365. if (imgs.length > 1) {
  366. let status_id = article.querySelector('a[href*="/status/"]').href.split('/status/').pop().split('/').shift()
  367. let btn_group = article.querySelector('div[role="group"]:last-of-type')
  368. let btn_share = Array.from(btn_group.querySelectorAll(':scope>div>div')).pop().parentNode
  369. imgs.forEach(img => {
  370. let index = img.href.split('/status/').pop().split('/').pop()
  371. let is_exist = history.indexOf(status_id) >= 0
  372. let btn_down = document.createElement('div')
  373. btn_down.innerHTML = '<div><div><svg viewBox="0 0 24 24" style="width: 18px; height: 18px;">' + this.svg + '</svg></div></div>'
  374. btn_down.classList.add('tmd-down', 'tmd-img')
  375. this.status(btn_down, 'download')
  376. img.parentNode.appendChild(btn_down)
  377. btn_down.onclick = e => {
  378. e.preventDefault()
  379. this.click(btn_down, status_id, is_exist, index)
  380. }
  381. })
  382. }
  383. },
  384. addButtonToMedia: function (listitems) {
  385. listitems.forEach(li => {
  386. if (li.dataset.detected) return
  387. li.dataset.detected = 'true'
  388. let status_id = li.querySelector('a[href*="/status/"]').href.split('/status/').pop().split('/').shift()
  389. let is_exist = history.indexOf(status_id) >= 0
  390. let btn_down = document.createElement('div')
  391. btn_down.innerHTML = '<div><div><svg viewBox="0 0 24 24" style="width: 18px; height: 18px;">' + this.svg + '</svg></div></div>'
  392. btn_down.classList.add('tmd-down', 'tmd-media')
  393. this.status(btn_down, is_exist ? 'completed' : 'download', is_exist ? lang.completed : lang.download)
  394. li.appendChild(btn_down)
  395. btn_down.onclick = () => this.click(btn_down, status_id, is_exist)
  396. })
  397. },
  398. click: async function (btn, status_id, is_exist, index) {
  399. if (btn.classList.contains('loading')) return
  400. this.status(btn, 'loading')
  401. let out = (await GM_getValue('filename', filename)).split('\n').join('')
  402. let save_history = await GM_getValue('save_history', true)
  403. let json = await this.fetchJson(status_id)
  404. let tweet = json.quoted_status_result?.result?.legacy?.media//此媒体存在,属于引用推文
  405. || json.quoted_status_result?.result?.legacy
  406. || json.legacy
  407. let user = json.core.user_results.result.legacy
  408. let invalid_chars = { '\\': '\', '\/': '/', '\|': '|', '<': '<', '>': '>', ':': ':', '*': '*', '?': '?', '"': '"', '\u200b': '', '\u200c': '', '\u200d': '', '\u2060': '', '\ufeff': '', '🔞': '' }
  409. let datetime = out.match(/\{date-time(-local)?:[^{}]+\}/) ? out.match(/\{date-time(?:-local)?:([^{}]+)\}/)[1].replace(/[\\/|<>*?:"]/g, v => invalid_chars[v]) : 'YYYYMMDD-hhmmss'
  410. let info = {}
  411. info['status-id'] = status_id
  412. info['user-name'] = user.name.replace(/([\\/|*?:"\u200b-\u200d\u2060\ufeff]|🔞)/g, v => invalid_chars[v])
  413. info['user-id'] = user.screen_name
  414. info['date-time'] = this.formatDate(tweet.created_at, datetime)
  415. info['date-time-local'] = this.formatDate(tweet.created_at, datetime, true)
  416. info['full-text'] = tweet.full_text.split('\n').join(' ').replace(/\s*https:\/\/t\.co\/\w+/g, '').replace(/[\\/|<>*?:"\u200b-\u200d\u2060\ufeff]/g, v => invalid_chars[v])
  417. let medias = tweet.extended_entities && tweet.extended_entities.media
  418. if (json?.card) {
  419. this.status(btn, 'failed', 'This tweet contains a link, which is not supported by this script.')
  420. return
  421. }
  422. if (!Array.isArray(medias)) {
  423. this.status(btn, 'failed', 'MEDIA_NOT_FOUND')
  424. return
  425. }
  426. if (index) medias = [medias[index - 1]]
  427. if (medias.length > 0) {
  428. let tasks = medias.map((media, i) => {
  429. info.url = media.type == 'photo' ? media.media_url_https + ':orig' : media.video_info.variants.filter(n => n.content_type == 'video/mp4').sort((a, b) => b.bitrate - a.bitrate)[0].url
  430. info.file = info.url.split('/').pop().split(/[:?]/).shift()
  431. info['file-name'] = info.file.split('.').shift()
  432. info['file-ext'] = info.file.split('.').pop()
  433. info['file-type'] = media.type.replace('animated_', '')
  434. info.out = (out.replace(/\.?\{file-ext\}/, '') + ((medias.length > 1 || index) && !out.match('{file-name}') ? '-' + (index ? index - 1 : i) : '') + '.{file-ext}').replace(/\{([^{}:]+)(:[^{}]+)?\}/g, (match, name) => info[name])
  435. return { url: info.url, name: info.out }
  436. })
  437. this.downloader.add(tasks, btn, save_history, is_exist, status_id, GM_getValue('enable_packaging', true))
  438. } else {
  439. this.status(btn, 'failed', 'MEDIA_NOT_FOUND')
  440. }
  441. }, downloader: (function () {
  442. let tasks = [], thread = 0, failed = 0, notifier, has_failed = false
  443. return {
  444. add: function (taskList, btn, save_history, is_exist, status_id, enable_packaging) {
  445. if (taskList.length > 1) {
  446. tasks.push(...taskList)
  447. this.update()
  448. if (enable_packaging) {
  449. let zip = new JSZip()
  450. let completedCount = 0
  451. taskList.forEach((task, i) => {
  452. thread++
  453. this.update()
  454. fetch(task.url)
  455. .then(response => response.blob())
  456. .then(blob => {
  457. zip.file(task.name, blob)
  458. tasks = tasks.filter(t => t.url !== task.url)
  459. thread--
  460. this.update()
  461. completedCount++
  462. if (completedCount === taskList.length) {
  463. zip.generateAsync({ type: 'blob' }).then(content => {
  464. let a = document.createElement('a')
  465. a.href = URL.createObjectURL(content)
  466. a.download = `${taskList[0].name}.zip`
  467. a.click()
  468. this.status(btn, 'completed', lang.completed)
  469. if (save_history && !is_exist) {
  470. history.push(status_id)
  471. this.storage(status_id)
  472. }
  473. })
  474. }
  475. })
  476. .catch(error => {
  477. failed++
  478. tasks = tasks.filter(t => t.url !== task.url)
  479. this.status(btn, 'failed', error.message)
  480. this.update()
  481. })
  482. })
  483. } else {
  484. taskList.forEach((task) => {
  485. thread++
  486. this.update()
  487.  
  488. GM_download({
  489. url: task.url,
  490. name: task.name,
  491. onload: () => {
  492. thread--
  493. tasks = tasks.filter(t => t.url !== task.url)
  494. this.status(btn, 'completed', lang.completed)
  495. if (save_history && !is_exist) {
  496. history.push(status_id)
  497. this.storage(status_id)
  498. }
  499. this.update()
  500. },
  501. onerror: result => {
  502. thread--
  503. failed++
  504. tasks = tasks.filter(t => t.url !== task.url)
  505. this.status(btn, 'failed', result.details.current)
  506. this.update()
  507. }
  508. })
  509. })
  510. }
  511. } else {
  512. tasks.push(taskList[0])
  513. thread++
  514. this.update()
  515. GM_download({
  516. url: taskList[0].url,
  517. name: taskList[0].name,
  518. onload: () => {
  519. thread--
  520. tasks = tasks.filter(t => t.url !== taskList[0].url)
  521. this.status(btn, 'completed', lang.completed)
  522.  
  523. if (save_history && !is_exist) {
  524. history.push(status_id)
  525. this.storage(status_id)
  526. }
  527. this.update()
  528. },
  529. onerror: result => {
  530. thread--
  531. failed++
  532. tasks = tasks.filter(t => t.url !== taskList[0].url)
  533. this.status(btn, 'failed', result.details.current)
  534. this.update()
  535. }
  536. })
  537. }
  538. },
  539. status: function (btn, css, title, style) {
  540. if (css) {
  541. btn.classList.remove('download', 'completed', 'loading', 'failed')
  542. btn.classList.add(css)
  543. }
  544. if (title) btn.title = title
  545. if (style) btn.style.cssText = style
  546. },
  547. storage: async function (value) {
  548. let data = await GM_getValue('download_history', [])
  549. let data_length = data.length
  550. if (value) {
  551. if (Array.isArray(value)) data = data.concat(value)
  552. else if (data.indexOf(value) < 0) data.push(value)
  553. } else return data
  554. if (data.length > data_length) GM_setValue('download_history', data)
  555. },
  556. update: function () {
  557. if (!notifier) {
  558. notifier = document.createElement('div')
  559. notifier.title = 'Twitter Media Downloader'
  560. notifier.classList.add('tmd-notifier')
  561. notifier.innerHTML = '<label>0</label>|<label>0</label>'
  562. document.body.appendChild(notifier)
  563. }
  564. if (failed > 0 && !has_failed) {
  565. has_failed = true
  566. notifier.innerHTML += '|'
  567. let clear = document.createElement('label')
  568. notifier.appendChild(clear)
  569. clear.onclick = () => {
  570. notifier.innerHTML = '<label>0</label>|<label>0</label>'
  571. failed = 0
  572. has_failed = false
  573. this.update()
  574. }
  575. }
  576. notifier.firstChild.innerText = thread
  577. notifier.firstChild.nextElementSibling.innerText = tasks.length - thread - failed
  578. if (failed > 0) notifier.lastChild.innerText = failed
  579. if (thread > 0 || tasks.length > 0 || failed > 0) notifier.classList.add('running')
  580. else notifier.classList.remove('running')
  581. }
  582. }
  583. })(),
  584. status: function (btn, css, title, style) {
  585. if (css) {
  586. btn.classList.remove('download', 'completed', 'loading', 'failed')
  587. btn.classList.add(css)
  588. }
  589. if (title) btn.title = title
  590. if (style) btn.style.cssText = style
  591. },
  592. settings: async function () {
  593. const $element = (parent, tag, style, content, css) => {
  594. let el = document.createElement(tag)
  595. if (style) el.style.cssText = style
  596. if (typeof content !== 'undefined') {
  597. if (tag == 'input') {
  598. if (content == 'checkbox') el.type = content
  599. else el.value = content
  600. } else el.innerHTML = content
  601. }
  602. if (css) css.split(' ').forEach(c => el.classList.add(c))
  603. parent.appendChild(el)
  604. return el
  605. }
  606. let wapper = $element(document.body, 'div', 'position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; background-color: #0009; z-index: 10;')
  607. let wapper_close
  608. wapper.onmousedown = e => {
  609. wapper_close = e.target == wapper
  610. }
  611. wapper.onmouseup = e => {
  612. if (wapper_close && e.target == wapper) wapper.remove()
  613. }
  614. let dialog = $element(wapper, 'div', 'position: absolute; left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%); width: fit-content; width: -moz-fit-content; background-color: #f3f3f3; border: 1px solid #ccc; border-radius: 10px; color: black;')
  615. let title = $element(dialog, 'h3', 'margin: 10px 20px;', lang.dialog.title)
  616. let options = $element(dialog, 'div', 'margin: 10px; border: 1px solid #ccc; border-radius: 5px;')
  617. let save_history_label = $element(options, 'label', 'display: block; margin: 10px;', lang.dialog.save_history)
  618. let save_history_input = $element(save_history_label, 'input', 'float: left;', 'checkbox')
  619. save_history_input.checked = await GM_getValue('save_history', true)
  620. save_history_input.onchange = () => {
  621. GM_setValue('save_history', save_history_input.checked)
  622. }
  623. let clear_history = $element(save_history_label, 'label', 'display: inline-block; margin: 0 10px; color: blue;', lang.dialog.clear_history)
  624. clear_history.onclick = () => {
  625. if (confirm(lang.dialog.clear_confirm)) {
  626. history = []
  627. GM_setValue('download_history', [])
  628. }
  629. }
  630. let show_sensitive_label = $element(options, 'label', 'display: block; margin: 10px;', lang.dialog.show_sensitive)
  631. let show_sensitive_input = $element(show_sensitive_label, 'input', 'float: left;', 'checkbox')
  632. show_sensitive_input.checked = await GM_getValue('show_sensitive', false)
  633. show_sensitive_input.onchange = () => {
  634. show_sensitive = show_sensitive_input.checked
  635. GM_setValue('show_sensitive', show_sensitive)
  636. }
  637. let show_enable_packaging = $element(options, 'label', 'display: block; margin: 10px;', lang.enable_packaging)
  638. let show_enable_packaging_input = $element(show_enable_packaging, 'input', 'float: left;', 'checkbox')
  639. show_enable_packaging_input.checked = await GM_getValue('enable_packaging', true)
  640. show_enable_packaging_input.onchange = () => {
  641. GM_setValue('enable_packaging', show_enable_packaging_input.checked)
  642. }
  643. let filename_div = $element(dialog, 'div', 'margin: 10px; border: 1px solid #ccc; border-radius: 5px;')
  644. let filename_label = $element(filename_div, 'label', 'display: block; margin: 10px 15px;', lang.dialog.pattern)
  645. let filename_input = $element(filename_label, 'textarea', 'display: block; min-width: 500px; max-width: 500px; min-height: 100px; font-size: inherit; background: white; color: black;', await GM_getValue('filename', filename))
  646. let filename_tags = $element(filename_div, 'label', 'display: table; margin: 10px;', `
  647. <span class="tmd-tag" title="user name">{user-name}</span>
  648. <span class="tmd-tag" title="The user name after @ sign.">{user-id}</span>
  649. <span class="tmd-tag" title="example: 1234567890987654321">{status-id}</span>
  650. <span class="tmd-tag" title="{date-time} : Posted time in UTC.\n{date-time-local} : Your local time zone.\n\nDefault:\nYYYYMMDD-hhmmss => 20201231-235959\n\nExample of custom:\n{date-time:DD-MMM-YY hh.mm} => 31-DEC-21 23.59">{date-time}</span><br>
  651. <span class="tmd-tag" title="Text content in tweet.">{full-text}</span>
  652. <span class="tmd-tag" title="Type of &#34;video&#34; or &#34;photo&#34; or &#34;gif&#34;.">{file-type}</span>
  653. <span class="tmd-tag" title="Original filename from URL.">{file-name}</span>
  654. `)
  655. filename_input.selectionStart = filename_input.value.length
  656. filename_tags.querySelectorAll('.tmd-tag').forEach(tag => {
  657. tag.onclick = () => {
  658. let ss = filename_input.selectionStart
  659. let se = filename_input.selectionEnd
  660. filename_input.value = filename_input.value.substring(0, ss) + tag.innerText + filename_input.value.substring(se)
  661. filename_input.selectionStart = ss + tag.innerText.length
  662. filename_input.selectionEnd = ss + tag.innerText.length
  663. filename_input.focus()
  664. }
  665. })
  666. let btn_save = $element(title, 'label', 'float: right;', lang.dialog.save, 'tmd-btn')
  667. btn_save.onclick = async () => {
  668. await GM_setValue('filename', filename_input.value)
  669. wapper.remove()
  670. }
  671. },
  672. fetchJson: async function (status_id) {
  673. let base_url = `https://${host}/i/api/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId`
  674. let variables = {
  675. 'tweetId': status_id,
  676. 'with_rux_injections': false,
  677. 'includePromotedContent': true,
  678. 'withCommunity': true,
  679. 'withQuickPromoteEligibilityTweetFields': true,
  680. 'withBirdwatchNotes': true,
  681. 'withVoice': true,
  682. 'withV2Timeline': true
  683. }
  684. let features = {
  685. 'articles_preview_enabled': true,
  686. 'c9s_tweet_anatomy_moderator_badge_enabled': true,
  687. 'communities_web_enable_tweet_community_results_fetch': false,
  688. 'creator_subscriptions_quote_tweet_preview_enabled': false,
  689. 'creator_subscriptions_tweet_preview_api_enabled': false,
  690. 'freedom_of_speech_not_reach_fetch_enabled': true,
  691. 'graphql_is_translatable_rweb_tweet_is_translatable_enabled': true,
  692. 'longform_notetweets_consumption_enabled': false,
  693. 'longform_notetweets_inline_media_enabled': true,
  694. 'longform_notetweets_rich_text_read_enabled': false,
  695. 'premium_content_api_read_enabled': false,
  696. 'profile_label_improvements_pcf_label_in_post_enabled': true,
  697. 'responsive_web_edit_tweet_api_enabled': false,
  698. 'responsive_web_enhance_cards_enabled': false,
  699. 'responsive_web_graphql_exclude_directive_enabled': false,
  700. 'responsive_web_graphql_skip_user_profile_image_extensions_enabled': false,
  701. 'responsive_web_graphql_timeline_navigation_enabled': false,
  702. 'responsive_web_grok_analysis_button_from_backend': false,
  703. 'responsive_web_grok_analyze_button_fetch_trends_enabled': false,
  704. 'responsive_web_grok_analyze_post_followups_enabled': false,
  705. 'responsive_web_grok_image_annotation_enabled': false,
  706. 'responsive_web_grok_share_attachment_enabled': false,
  707. 'responsive_web_grok_show_grok_translated_post': false,
  708. 'responsive_web_jetfuel_frame': false,
  709. 'responsive_web_media_download_video_enabled': false,
  710. 'responsive_web_twitter_article_tweet_consumption_enabled': true,
  711. 'rweb_tipjar_consumption_enabled': true,
  712. 'rweb_video_screen_enabled': false,
  713. 'standardized_nudges_misinfo': true,
  714. 'tweet_awards_web_tipping_enabled': false,
  715. 'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled': true,
  716. 'tweetypie_unmention_optimization_enabled': false,
  717. 'verified_phone_label_enabled': false,
  718. 'view_counts_everywhere_api_enabled': true
  719. }
  720. let url = encodeURI(`${base_url}?variables=${JSON.stringify(variables)}&features=${JSON.stringify(features)}`)
  721. let cookies = this.getCookie()
  722. let headers = {
  723. 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  724. 'x-twitter-active-user': 'yes',
  725. 'x-twitter-client-language': cookies.lang,
  726. 'x-csrf-token': cookies.ct0
  727. }
  728. if (cookies.ct0.length == 32) headers['x-guest-token'] = cookies.gt
  729. let tweet_detail = await fetch(url, { headers: headers }).then(result => result.json())
  730. let tweet_result = tweet_detail.data.tweetResult.result
  731. return tweet_result.tweet || tweet_result
  732. },
  733. getCookie: function (name) {
  734. let cookies = {}
  735. document.cookie.split(';').filter(n => n.indexOf('=') > 0).forEach(n => {
  736. n.replace(/^([^=]+)=(.+)$/, (match, name, value) => {
  737. cookies[name.trim()] = value.trim()
  738. })
  739. })
  740. return name ? cookies[name] : cookies
  741. },
  742. storage: async function (value) {
  743. let data = await GM_getValue('download_history', [])
  744. let data_length = data.length
  745. if (value) {
  746. if (Array.isArray(value)) data = data.concat(value)
  747. else if (data.indexOf(value) < 0) data.push(value)
  748. } else return data
  749. if (data.length > data_length) GM_setValue('download_history', data)
  750. },
  751. storage_obsolete: function (is_remove) {
  752. let data = JSON.parse(localStorage.getItem('history') || '[]')
  753. if (is_remove) localStorage.removeItem('history')
  754. else return data
  755. },
  756. formatDate: function (i, o, tz) {
  757. let d = new Date(i)
  758. if (tz) d.setMinutes(d.getMinutes() - d.getTimezoneOffset())
  759. let m = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
  760. let v = {
  761. YYYY: d.getUTCFullYear().toString(),
  762. YY: d.getUTCFullYear().toString(),
  763. MM: d.getUTCMonth() + 1,
  764. MMM: m[d.getUTCMonth()],
  765. DD: d.getUTCDate(),
  766. hh: d.getUTCHours(),
  767. mm: d.getUTCMinutes(),
  768. ss: d.getUTCSeconds(),
  769. h2: d.getUTCHours() % 12,
  770. ap: d.getUTCHours() < 12 ? 'AM' : 'PM'
  771. }
  772. return o.replace(/(YY(YY)?|MMM?|DD|hh|mm|ss|h2|ap)/g, n => ('0' + v[n]).substr(-n.length))
  773. },
  774.  
  775. language: {
  776. en: { download: 'Download', completed: 'Download Completed', settings: 'Settings', dialog: { title: 'Download Settings', save: 'Save', save_history: 'Remember download history', clear_history: '(Clear)', clear_confirm: 'Clear download history?', show_sensitive: 'Always show sensitive content', pattern: 'File Name Pattern' }, enable_packaging: 'Package multiple files into a ZIP' },
  777. ja: { download: 'ダウンロード', completed: 'ダウンロード完了', settings: '設定', dialog: { title: 'ダウンロード設定', save: '保存', save_history: 'ダウンロード履歴を保存する', clear_history: '(クリア)', clear_confirm: 'ダウンロード履歴を削除する?', show_sensitive: 'センシティブな内容を常に表示する', pattern: 'ファイル名パターン' }, enable_packaging: '複数ファイルを ZIP にパッケージ化する' },
  778. zh: { download: '下载', completed: '下载完成', settings: '设置', dialog: { title: '下载设置', save: '保存', save_history: '保存下载记录', clear_history: '(清除)', clear_confirm: '确认要清除下载记录?', show_sensitive: '自动显示敏感的内容', pattern: '文件名格式' }, enable_packaging: '多文件打包成 ZIP' },
  779. 'zh-Hant': { download: '下載', completed: '下載完成', settings: '設置', dialog: { title: '下載設置', save: '保存', save_history: '保存下載記錄', clear_history: '(清除)', clear_confirm: '確認要清除下載記錄?', show_sensitive: '自動顯示敏感的内容', pattern: '文件名規則' }, enable_packaging: '多文件打包成 ZIP' }
  780. },
  781. css: `
  782. .tmd-down {margin-left: 12px; order: 99;}
  783. .tmd-down:hover > div > div > div > div {color: rgba(29, 161, 242, 1.0);}
  784. .tmd-down:hover > div > div > div > div > div {background-color: rgba(29, 161, 242, 0.1);}
  785. .tmd-down:active > div > div > div > div > div {background-color: rgba(29, 161, 242, 0.2);}
  786. .tmd-down:hover svg {color: rgba(29, 161, 242, 1.0);}
  787. .tmd-down:hover div:first-child:not(:last-child) {background-color: rgba(29, 161, 242, 0.1);}
  788. .tmd-down:active div:first-child:not(:last-child) {background-color: rgba(29, 161, 242, 0.2);}
  789. .tmd-down.tmd-media {position: absolute; right: 0;}
  790. .tmd-down.tmd-media > div {display: flex; border-radius: 99px; margin: 2px;}
  791. .tmd-down.tmd-media > div > div {display: flex; margin: 6px; color: #fff;}
  792. .tmd-down.tmd-media:hover > div {background-color: rgba(255,255,255, 0.6);}
  793. .tmd-down.tmd-media:hover > div > div {color: rgba(29, 161, 242, 1.0);}
  794. .tmd-down.tmd-media:not(:hover) > div > div {filter: drop-shadow(0 0 1px #000);}
  795. .tmd-down g {display: none;}
  796. .tmd-down.download g.download, .tmd-down.completed g.completed, .tmd-down.loading g.loading,.tmd-down.failed g.failed {display: unset;}
  797. .tmd-down.loading svg {animation: spin 1s linear infinite;}
  798. @keyframes spin {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}}
  799. .tmd-btn {display: inline-block; background-color: #1DA1F2; color: #FFFFFF; padding: 0 20px; border-radius: 99px;}
  800. .tmd-tag {display: inline-block; background-color: #FFFFFF; color: #1DA1F2; padding: 0 10px; border-radius: 10px; border: 1px solid #1DA1F2; font-weight: bold; margin: 5px;}
  801. .tmd-btn:hover {background-color: rgba(29, 161, 242, 0.9);}
  802. .tmd-tag:hover {background-color: rgba(29, 161, 242, 0.1);}
  803. .tmd-notifier {display: none; position: fixed; left: 16px; bottom: 16px; color: #000; background: #fff; border: 1px solid #ccc; border-radius: 8px; padding: 4px;}
  804. .tmd-notifier.running {display: flex; align-items: center;}
  805. .tmd-notifier label {display: inline-flex; align-items: center; margin: 0 8px;}
  806. .tmd-notifier label:before {content: " "; width: 32px; height: 16px; background-position: center; background-repeat: no-repeat;}
  807. .tmd-notifier label:nth-child(1):before {background-image:url("data:image/svg+xml;charset=utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22><path d=%22M3,14 v5 q0,2 2,2 h14 q2,0 2,-2 v-5 M7,10 l4,4 q1,1 2,0 l4,-4 M12,3 v11%22 fill=%22none%22 stroke=%22%23666%22 stroke-width=%222%22 stroke-linecap=%22round%22 /></svg>");}
  808. .tmd-notifier label:nth-child(2):before {background-image:url("data:image/svg+xml;charset=utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22><path d=%22M12,2 a1,1 0 0 1 0,20 a1,1 0 0 1 0,-20 M12,5 v7 h6%22 fill=%22none%22 stroke=%22%23999%22 stroke-width=%222%22 stroke-linejoin=%22round%22 stroke-linecap=%22round%22 /></svg>");}
  809. .tmd-notifier label:nth-child(3):before {background-image:url("data:image/svg+xml;charset=utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22><path d=%22M12,0 a2,2 0 0 0 0,24 a2,2 0 0 0 0,-24%22 fill=%22%23f66%22 stroke=%22none%22 /><path d=%22M14.5,5 a1,1 0 0 0 -5,0 l0.5,9 a1,1 0 0 0 4,0 z M12,17 a2,2 0 0 0 0,5 a2,2 0 0 0 0,-5%22 fill=%22%23fff%22 stroke=%22none%22 /></svg>");}
  810. .tmd-down.tmd-img {position: absolute; right: 0; bottom: 0; display: none !important;}
  811. .tmd-down.tmd-img > div {display: flex; border-radius: 99px; margin: 2px; background-color: rgba(255,255,255, 0.6);}
  812. .tmd-down.tmd-img > div > div {display: flex; margin: 6px; color: #fff !important;}
  813. .tmd-down.tmd-img:not(:hover) > div > div {filter: drop-shadow(0 0 1px #000);}
  814. .tmd-down.tmd-img:hover > div > div {color: rgba(29, 161, 242, 1.0);}
  815. :hover > .tmd-down.tmd-img, .tmd-img.loading, .tmd-img.completed, .tmd-img.failed {display: block !important;}
  816. .tweet-detail-action-item {width: 20% !important;}
  817. `,
  818. css_ss: `
  819. /* show sensitive in media tab */
  820. li[role="listitem"]>div>div>div>div:not(:last-child) {filter: none;}
  821. li[role="listitem"]>div>div>div>div+div:last-child {display: none;}
  822. `,
  823. svg: `
  824. <g class="download"><path d="M3,14 v5 q0,2 2,2 h14 q2,0 2,-2 v-5 M7,10 l4,4 q1,1 2,0 l4,-4 M12,3 v11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></g>
  825. <g class="completed"><path d="M3,14 v5 q0,2 2,2 h14 q2,0 2,-2 v-5 M7,10 l3,4 q1,1 2,0 l8,-11" fill="none" stroke="#1DA1F2" stroke-width="2" stroke-linecap="round" /></g>
  826. <g class="loading"><circle cx="12" cy="12" r="10" fill="none" stroke="#1DA1F2" stroke-width="4" opacity="0.4" /><path d="M12,2 a10,10 0 0 1 10,10" fill="none" stroke="#1DA1F2" stroke-width="4" stroke-linecap="round" /></g>
  827. <g class="failed"><circle cx="12" cy="12" r="11" fill="#f33" stroke="currentColor" stroke-width="2" opacity="0.8" /><path d="M14,5 a1,1 0 0 0 -4,0 l0.5,9.5 a1.5,1.5 0 0 0 3,0 z M12,17 a2,2 0 0 0 0,4 a2,2 0 0 0 0,-4" fill="#fff" stroke="none" /></g>
  828. `
  829. }
  830. })()
  831.  
  832. TMD.init()