.tx
android
qtc_packaging
+debian
+
+2.4
+- Now using YouTube APIs version 3
+- Now using HTTPS everywhere for better privacy and security
+- Minitube now automatically loads more videos in the playlist for even less clicking
+- Updated media engine to LibVLC 2.2.0 on Mac & Windows
+- Fixed subscriptions sorting
+- Fixed toolbar style on Ubuntu
+- New and updated translations
+
+2.3.1
+- Fix playback problems with some videos
+
2.3
- Take video snapshots at full resolution
- Faster and more reliable seeking
- Slide transition in playlist navigation
- Make the volume handle red when volume is zero
- Enhancements to the search suggestions
+- Style refresh to make Minitube feel at home on OS X Yosemite and Windows 8
- The Mac version is now 64bit and uses the VLC engine to play videos
- Fixed minor style issues with Mac OS X Yosemite 10.10
- Restored compatibility with Mac OS X Snow Leopard 10.6
+++ /dev/null
-# Build instructions
-
-## Prerequisites
-To compile Minitube you need at least Qt 4.5, Qt >= 4.6 is recommended.
-The following Qt modules are needed: core, gui, network, sql, script, xml, dbus, phonon
-
-On a Debian or Ubuntu system type:
-sudo apt-get install build-essential qt4-dev-tools libphonon-dev
-
-## Compiling
-Run:
- $ qmake
-and then:
- $ make
-Beware of the Qt3 or Qt5 version of qmake! If things go wrong try running qmake-qt4 instead.
-
-## Running
-./build/target/minitube
-
-## A word about Phonon on Linux
-To be able to actually watch videos you need a working Phonon setup.
-Please don't contact me about this, ask for help on your distribution support channels.
-
-These days Minitube is tested with the VLC backend only.
-Please don't report bugs with other backends as they're not supported.
-
-## Installing on Linux
-Run:
- $ sudo make install
-This is for packagers. End users should not install applications in this way.
-
-## Legal Stuff
-Copyright (C) Flavio Tordini
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
--- /dev/null
+# Minitube
+Minitube is a YouTube desktop application. It is written in C++ using the Qt framework. Contributing is welcome, especially in the Linux desktop integration area.
+
+## Traslating Minitube to your language
+Minitube translations are done at https://www.transifex.com/projects/p/minitube/
+Just register and apply for a language team. Please don't request translation merges on GitHub.
+
+## Building
+To compile Minitube you need at least Qt 4.8. The following Qt modules are needed:
+core, gui, network, sql (using the Sqlite plugin), script, dbus, phonon
+
+On a Debian or Ubuntu system, type:
+
+ $ sudo apt-get install build-essential qt4-dev-tools libphonon-dev
+
+### Google API Key
+
+Google is now requiring an API key in order to access YouTube Data web services.
+Create a "Browser Key" at https://console.developers.google.com
+
+The key must be specified at compile time as shown below.
+Alternatively Minitube can read an API key from the GOOGLE_API_KEY environment variable.
+
+### Compiling
+Run:
+
+ $ qmake -DAPP_GOOGLE_API_KEY="YouAPIKeyHere"
+
+and then:
+
+ $ make
+
+Beware of the Qt3 or Qt5 version of qmake! If things go wrong try running qmake-qt4 instead.
+
+### Running
+
+ $ ./build/target/minitube
+
+### Installing on Linux
+
+ $ sudo make install
+
+This is for packagers. End users should not install applications in this way.
+
+## A word about Phonon on Linux
+To be able to actually watch videos you need a working Phonon setup.
+Please don't contact me about this, ask for help on your distribution support channels.
+
+These days Minitube is tested with the VLC backend only.
+Please don't report bugs with other backends as they're not supported.
+
+## Legal Stuff
+Copyright (C) Flavio Tordini
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>La versió completa permet visualitzar videos sense interrupcions.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Marca com a Visualitzat</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Donar-se de baixa</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>Pren &Instantània</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Instantània desada a %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>La ubicació de les instantànies ha canviat. </translation>
</message>
</context>
<context>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>Den fulde version giver dig mulighed for, at se videoer uden afbrydelser.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Marker som set</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Afmeld</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>Tag $Skærmbillede</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>%Vild med %1? Bedøm det!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Skærmbillede gemt på %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>Lokation for skærmbilleder ændret.</translation>
</message>
</context>
<context>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>Η πλήρης έκδοση σας επιτρέπει να παρακολουθήσετε βίντεο χωρίς διακοπές.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Σήμανση ως Παρακολουθημένο</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Κατάργηση εγγραφής</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<source>There are no updated subscriptions at this time.</source>
- <translation>Î\94εν Ï\85Ï\80άÏ\81Ï\87οÏ\85ν ενημεÏ\81Ï\89μÎνεÏ\82 Ï\83Ï\85νδÏ\81ομÎÏ\82 αÏ\85Ï\84ήν Ï\84ην Ï\83Ï\84ιγμή.</translation>
+ <translation>Δεν υπάρχουν ενημερωμένες συδρομές αυτήν την στιγμή.</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="271"/>
<message>
<location filename="src/homeview.cpp" line="73"/>
<source>Channel subscriptions</source>
- <translation>ΣÏ\85νδÏ\81ομÎÏ\82 Ï\83ε κανάλια</translation>
+ <translation>Συδρομές σε κανάλια</translation>
</message>
<message>
<location filename="src/homeview.h" line="45"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>Λήψη &Στιγμιότυπου</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<source>&Subscribe to Channel</source>
- <translation>&ΣÏ\85δÏ\81Î¿Î¼ή στο Κανάλι</translation>
+ <translation>&Î\95γγÏ\81αÏ\86ή στο Κανάλι</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="501"/>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>&Λατρεύετε το %1? Βαθμολογήστε το!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/mediaview.cpp" line="1109"/>
<source>Unsubscribe from %1</source>
- <translation>Κατάργηση συνδρομής στο %1</translation>
+ <translation>Κατάργηση εγγραφής στο %1</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="1113"/>
<source>Subscribe to %1</source>
- <translation>ΣÏ\85νδÏ\81Î¿Î¼ή στο %1</translation>
+ <translation>Î\95γγÏ\81αÏ\86ή στο %1</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="788"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Το στιγμιότυπο αποθηκεύτηκε σε %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>Η τοποθεσία για στιγμιότυπα άλλαξε.</translation>
</message>
</context>
<context>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>Täysversio mahdollistaa videoiden katselun ilman keskeytyksiä.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Merkitse katsotuksi</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Lopeta tilaus</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>Ota &tilannevedos</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Tilannevedos tallennettu polkuun %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>Tilannevedoksien sijainti muutettu.</translation>
</message>
</context>
<context>
<message>
<location filename="src/aboutview.h" line="40"/>
<source>About</source>
- <translation>×¢×\9c ×\90×\95×\93×\95ת</translation>
+ <translation>אודות</translation>
</message>
<message>
<location filename="src/aboutview.h" line="42"/>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>הגירסה המלאה מאפשרת לך לצפות בסרטון ללא הפרעות.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelaggregator.cpp" line="131"/>
<source>By %1</source>
- <translation type="unfinished"/>
+ <translation>על ידי %1</translation>
</message>
<message numerus="yes">
<location filename="src/channelaggregator.cpp" line="133"/>
<message>
<location filename="src/channelitemdelegate.cpp" line="66"/>
<source>All Videos</source>
- <translation type="unfinished"/>
+ <translation>כל הסרטונים</translation>
</message>
<message>
<location filename="src/channelitemdelegate.cpp" line="83"/>
<source>Unwatched Videos</source>
- <translation type="unfinished"/>
+ <translation>סרטונים שלא נצפו</translation>
</message>
</context>
<context>
<message>
<location filename="src/channelview.cpp" line="151"/>
<source>Mark all as watched</source>
- <translation type="unfinished"/>
+ <translation>סמנו את כולם כנצפו</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="159"/>
<source>Show Updated</source>
- <translation type="unfinished"/>
+ <translation>הצג עדכונים</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="103"/>
<source>Name</source>
- <translation type="unfinished"/>
+ <translation>שם</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="110"/>
<source>Last Updated</source>
- <translation type="unfinished"/>
+ <translation>עודכן לאחרונה</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="117"/>
<source>Last Added</source>
- <translation type="unfinished"/>
+ <translation>נוסף לאחרונה</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="124"/>
<source>Last Watched</source>
- <translation type="unfinished"/>
+ <translation>נצפה לאחרונה</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="131"/>
<source>Most Watched</source>
- <translation type="unfinished"/>
+ <translation>נצפה ביותר</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="139"/>
<source>Sort by</source>
- <translation type="unfinished"/>
+ <translation>מיין לפי</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="221"/>
<source>All Videos</source>
- <translation type="unfinished"/>
+ <translation>כל הסרטונים</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="225"/>
<source>Unwatched Videos</source>
- <translation type="unfinished"/>
+ <translation>סרטונים שלא נצפו</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>סמן כנצפה</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>בטל מינוי</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<source>There are no updated subscriptions at this time.</source>
- <translation type="unfinished"/>
+ <translation>אין עדכונים בערוצים שאתה מנוי עליהם כרגע.</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="271"/>
<source>You have no subscriptions. Use the star symbol to subscribe to channels.</source>
- <translation type="unfinished"/>
+ <translation>אין לך מנויים. השתמש בסמל הכוכב על מנת להיות מנוי לערוצים.</translation>
</message>
</context>
<context>
<message>
<location filename="src/homeview.cpp" line="71"/>
<source>Subscriptions</source>
- <translation type="unfinished"/>
+ <translation>מנויים</translation>
</message>
<message>
<location filename="src/homeview.cpp" line="73"/>
<source>Channel subscriptions</source>
- <translation type="unfinished"/>
+ <translation>מנויים בערוצים</translation>
</message>
<message>
<location filename="src/homeview.h" line="45"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>קח צילום מסך</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<source>&Subscribe to Channel</source>
- <translation type="unfinished"/>
+ <translation>הרשם כמנוי לערוץ</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="501"/>
<source>Share the current video using %1</source>
- <translation>ש×\99ת×\95×£ ×\94×\95×\95×\99×\93×\90×\95 ×\94× ×\95×\9b×\97×\99 ×\93ר×\9a %1</translation>
+ <translation>ש×\99ת×\95×£ ×\94×\95×\95×\99×\93×\90×\95 ×\94× ×\95×\9b×\97×\99 ×\91×\90×\9eצע×\95ת %1</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="521"/>
<message>
<location filename="src/mainwindow.cpp" line="568"/>
<source>&Related Videos</source>
- <translation type="unfinished"/>
+ <translation>סרטונים דומים</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="570"/>
<source>Watch videos related to the current one</source>
- <translation type="unfinished"/>
+ <translation>צפה בסרטונים הדומים לסרטון זה</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="576"/>
<source>Open in &Browser...</source>
- <translation type="unfinished"/>
+ <translation>פתח בדפדפן</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>&אוהב את %1? דרג אותו!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/mainwindow.cpp" line="397"/>
<source>Make a &Donation</source>
- <translation>ה&גשת תרומה</translation>
+ <translation>&תרמו</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="459"/>
<message>
<location filename="src/mainwindow.cpp" line="797"/>
<source>Choose your content location</source>
- <translation type="unfinished"/>
+ <translation>בחר את המיקום הנוכחי שלך</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1073"/>
<message>
<location filename="src/mediaview.cpp" line="682"/>
<source>You can now paste the YouTube link into another application</source>
- <translation>×\9bעת × ×\99ת×\9f ×\9c×\94×\93×\91×\99ק ×\90ת ק×\99ש×\95ר ×\94Ö¾YouTube ש×\9c×\9b×\9d ×\9cיישום אחר</translation>
+ <translation>×\9bעת × ×\99ת×\9f ×\9c×\94×\93×\91×\99ק ×\90ת ק×\99ש×\95ר ×\94Ö¾YouTube ש×\9c×\9b×\9d ×\91יישום אחר</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="690"/>
<message>
<location filename="src/mediaview.cpp" line="1109"/>
<source>Unsubscribe from %1</source>
- <translation type="unfinished"/>
+ <translation>בטל מנוי מ %1</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="1113"/>
<source>Subscribe to %1</source>
- <translation type="unfinished"/>
+ <translation>מנוי ל %1</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="788"/>
<message>
<location filename="local/src/updatedialog.cpp" line="21"/>
<source>A new version of %1 is available!</source>
- <translation type="unfinished"/>
+ <translation>גירסה חדשה של %1 זמינה להורדה!</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="28"/>
<source>%1 %2 is now available. You have %3.</source>
- <translation type="unfinished"/>
+ <translation>%1 %2 זמין כעת. לך יש %3.</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="33"/>
<message>
<location filename="src/playlistitemdelegate.cpp" line="332"/>
<source>%1 of %2 (%3) — %4</source>
- <translation type="unfinished"/>
+ <translation>%1 מתוך %2 (%3) — %4</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="339"/>
<source>Preparing</source>
- <translation type="unfinished"/>
+ <translation>מתכונן</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="341"/>
<source>Failed</source>
- <translation type="unfinished"/>
+ <translation>נכשל</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="343"/>
<source>Completed</source>
- <translation type="unfinished"/>
+ <translation>הושלם</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="345"/>
<source>Stopped</source>
- <translation type="unfinished"/>
+ <translation>הופסק</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="381"/>
<source>Stop downloading</source>
- <translation type="unfinished"/>
+ <translation>עצור הורדות</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="391"/>
<source>Show in %1</source>
- <translation type="unfinished"/>
+ <translation>הצג ב %1</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="393"/>
<source>Open parent folder</source>
- <translation type="unfinished"/>
+ <translation>פתח תיקיית מקור</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="402"/>
<source>Restart downloading</source>
- <translation type="unfinished"/>
+ <translation>התחל הורדות מחדש</translation>
</message>
</context>
<context>
<location filename="src/sidebarheader.cpp" line="39"/>
<location filename="src/sidebarheader.cpp" line="46"/>
<source>&Back</source>
- <translation type="unfinished"/>
+ <translation>הקודם</translation>
</message>
<message>
<location filename="src/sidebarheader.cpp" line="77"/>
<source>Forward to %1</source>
- <translation type="unfinished"/>
+ <translation>העבר אל %1</translation>
</message>
<message>
<location filename="src/sidebarheader.cpp" line="90"/>
<source>Back to %1</source>
- <translation type="unfinished"/>
+ <translation>חזור אל %1</translation>
</message>
</context>
<context>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>צילום מסך נשמר ב %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>מיקום צילומי מסך שונה.</translation>
</message>
</context>
<context>
<message>
<location filename="src/standardfeedsview.cpp" line="105"/>
<source>Most Popular</source>
- <translation type="unfinished"/>
+ <translation>הכי פופולאריים</translation>
</message>
</context>
<context>
<message>
<location filename="src/aboutview.cpp" line="58"/>
<source>Licensed to: %1</source>
- <translation type="unfinished"/>
+ <translation>Licensz tulajdonosa: %1</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="62"/>
<message>
<location filename="local/src/activationdialog.cpp" line="17"/>
<source>Enter your License Details</source>
- <translation type="unfinished"/>
+ <translation>Liszenszadatok megadása</translation>
</message>
<message>
<location filename="local/src/activationdialog.cpp" line="29"/>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>A teljes verzió lehetőséget nyújt a videók megszakítás nélküli lejátszására.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="local/src/activationview.cpp" line="85"/>
<source>Buy License</source>
- <translation type="unfinished"/>
+ <translation>Liszensz megvásárlása</translation>
</message>
</context>
<context>
<message>
<location filename="src/channelaggregator.cpp" line="131"/>
<source>By %1</source>
- <translation type="unfinished"/>
+ <translation>%1 által</translation>
</message>
<message numerus="yes">
<location filename="src/channelaggregator.cpp" line="133"/>
<message>
<location filename="src/channelitemdelegate.cpp" line="83"/>
<source>Unwatched Videos</source>
- <translation type="unfinished"/>
+ <translation>Megnézetlen videók</translation>
</message>
</context>
<context>
<message>
<location filename="src/channelview.cpp" line="151"/>
<source>Mark all as watched</source>
- <translation type="unfinished"/>
+ <translation>Mind megjelölése megnézettként</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="159"/>
<source>Show Updated</source>
- <translation type="unfinished"/>
+ <translation>Frissítések mutatása</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="103"/>
<message>
<location filename="src/channelview.cpp" line="110"/>
<source>Last Updated</source>
- <translation type="unfinished"/>
+ <translation>Utoljára frissítve</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="117"/>
<source>Last Added</source>
- <translation type="unfinished"/>
+ <translation>Utoljára hozzáadva</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="124"/>
<source>Last Watched</source>
- <translation type="unfinished"/>
+ <translation>Utoljára megnézve</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="131"/>
<source>Most Watched</source>
- <translation type="unfinished"/>
+ <translation>Legtöbbször megnézett</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="139"/>
<message>
<location filename="src/channelview.cpp" line="225"/>
<source>Unwatched Videos</source>
- <translation type="unfinished"/>
+ <translation>Megnézetlen videók</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Megjelölés megnézettként</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Leiratkozás</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<source>There are no updated subscriptions at this time.</source>
- <translation type="unfinished"/>
+ <translation>Nincs frissítés a feliratkozott csatornákon.</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="271"/>
<message>
<location filename="src/downloadmanager.cpp" line="156"/>
<source>%1 downloaded in %2</source>
- <translation type="unfinished"/>
+ <translation>%1 letöltve ide: %2</translation>
</message>
<message>
<location filename="src/downloadmanager.cpp" line="159"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>&Snapshot készítése</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<source>&Subscribe to Channel</source>
- <translation type="unfinished"/>
+ <translation>&Subscribe a csatornára</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="501"/>
<message>
<location filename="src/mainwindow.cpp" line="565"/>
<source>More...</source>
- <translation type="unfinished"/>
+ <translation>Továbbiak...</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="568"/>
<message>
<location filename="src/mainwindow.cpp" line="570"/>
<source>Watch videos related to the current one</source>
- <translation type="unfinished"/>
+ <translation>A jelenlegihez kapcsolódó videók megtekintése</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="576"/>
<source>Open in &Browser...</source>
- <translation type="unfinished"/>
+ <translation>Megtekintés a következővel:</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>&Love %1? Értékeld!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/mainwindow.cpp" line="797"/>
<source>Choose your content location</source>
- <translation type="unfinished"/>
+ <translation>Válaszd ki a jelenlegi tartózkodási helyed</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1073"/>
<message>
<location filename="src/mediaview.cpp" line="1109"/>
<source>Unsubscribe from %1</source>
- <translation type="unfinished"/>
+ <translation>Leiratkozás az alábbi csatornáról: %1</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="1113"/>
<source>Subscribe to %1</source>
- <translation type="unfinished"/>
+ <translation>Feliratkozás az alábbi csatornára: %1</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="788"/>
<message>
<location filename="local/src/updatedialog.cpp" line="21"/>
<source>A new version of %1 is available!</source>
- <translation type="unfinished"/>
+ <translation>%1 új verziója elérhető!</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="28"/>
<message>
<location filename="local/src/updatedialog.cpp" line="39"/>
<source>Skip This Version</source>
- <translation type="unfinished"/>
+ <translation>Verzió átugrása</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="43"/>
<message>
<location filename="src/playlistmodel.cpp" line="73"/>
<source>Show %1 More</source>
- <translation type="unfinished"/>
+ <translation>További %1 elem megjelenítése</translation>
</message>
<message>
<location filename="src/playlistmodel.cpp" line="74"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Pillanatkép mentve a %1 helyre</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>A pillanatképek mentési helye megváltozott.</translation>
</message>
</context>
<context>
<message>
<location filename="src/aboutview.cpp" line="58"/>
<source>Licensed to: %1</source>
- <translation type="unfinished"/>
+ <translation>ライセンス:1</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="62"/>
<message>
<location filename="src/aboutview.cpp" line="63"/>
<source>Please <a href='%1'>donate</a> to support the continued development of %2.</source>
- <translation type="unfinished"/>
+ <translation>継続的な開発を<a href='%1'>支援する</a>には%2に寄付してください。</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="67"/>
<source>You may want to try my other apps as well:</source>
- <translation>私が作った他のアプリケーションにも興味がありますか。</translation>
+ <translation>私が作成した他のアプリケーションはこちら:</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="70"/>
<source>%1, a YouTube music player</source>
- <translation type="unfinished"/>
+ <translation>%1はYoutube音楽プレイヤーです</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="74"/>
<source>%1, a music player</source>
- <translation>%1, ミュージックプレイヤー</translation>
+ <translation>%1は音楽プレイヤーです</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="80"/>
<source>Translate %1 to your native language using %2</source>
- <translation>%2を使って、%1をあなたの母語に翻訳してください。</translation>
+ <translation>%2を使って、%1をあなたの言語に翻訳してください</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="85"/>
<message>
<location filename="src/aboutview.h" line="40"/>
<source>About</source>
- <translation>プログラムについて</translation>
+ <translation>Minitubeについて</translation>
</message>
<message>
<location filename="src/aboutview.h" line="42"/>
<message>
<location filename="local/src/activationdialog.cpp" line="17"/>
<source>Enter your License Details</source>
- <translation type="unfinished"/>
+ <translation>ライセンスコードを入力してください</translation>
</message>
<message>
<location filename="local/src/activationdialog.cpp" line="29"/>
<message>
<location filename="local/src/activationview.cpp" line="47"/>
<source>Please license %1</source>
- <translation type="unfinished"/>
+ <translation>%1のライセンスを取得してください</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="51"/>
<source>This demo has expired.</source>
- <translation type="unfinished"/>
+ <translation>試用版の期限が切れました。</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>製品版では、快適に動画を視聴することができます。</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<source>Without a license, the application will expire in %1 days.</source>
- <translation type="unfinished"/>
+ <translation>現在は、ライセンスがないため、このアプリケーションは%1日に期限切れとなります。</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="57"/>
<source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
- <translation type="unfinished"/>
+ <translation>あなたが製品版を購入することによって、%1の開発を支援することができます。</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="70"/>
<source>Use Demo</source>
- <translation>デモを使用</translation>
+ <translation>試用版を使用</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="77"/>
<message>
<location filename="src/channelaggregator.cpp" line="131"/>
<source>By %1</source>
- <translation type="unfinished"/>
+ <translation>%1による</translation>
</message>
<message numerus="yes">
<location filename="src/channelaggregator.cpp" line="133"/>
<source>You have %n new video(s)</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n件の新しい動画があります</numerusform></translation>
</message>
</context>
<context>
<message>
<location filename="src/channelitemdelegate.cpp" line="66"/>
<source>All Videos</source>
- <translation type="unfinished"/>
+ <translation>すべてのチャンネル</translation>
</message>
<message>
<location filename="src/channelitemdelegate.cpp" line="83"/>
<source>Unwatched Videos</source>
- <translation type="unfinished"/>
+ <translation>未視聴の動画</translation>
</message>
</context>
<context>
<message>
<location filename="src/channelview.cpp" line="151"/>
<source>Mark all as watched</source>
- <translation type="unfinished"/>
+ <translation>視聴済みの動画をマーク</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="159"/>
<source>Show Updated</source>
- <translation type="unfinished"/>
+ <translation>動画の更新を確認</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="103"/>
<source>Name</source>
- <translation type="unfinished"/>
+ <translation>名前順</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="110"/>
<source>Last Updated</source>
- <translation type="unfinished"/>
+ <translation>更新日が新しい順</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="117"/>
<source>Last Added</source>
- <translation type="unfinished"/>
+ <translation>購読日が新しい順</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="124"/>
<source>Last Watched</source>
- <translation type="unfinished"/>
+ <translation>視聴日が新しい順</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="131"/>
<source>Most Watched</source>
- <translation type="unfinished"/>
+ <translation>視聴回数が多い順</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="139"/>
<source>Sort by</source>
- <translation type="unfinished"/>
+ <translation>並び替え</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="221"/>
<source>All Videos</source>
- <translation type="unfinished"/>
+ <translation>全ての動画</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="225"/>
<source>Unwatched Videos</source>
- <translation type="unfinished"/>
+ <translation>未視聴の動画</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>見たものをマーク</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>購読を解除</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<source>There are no updated subscriptions at this time.</source>
- <translation type="unfinished"/>
+ <translation>購読したチャンネルに更新はありません。</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="271"/>
<source>You have no subscriptions. Use the star symbol to subscribe to channels.</source>
- <translation type="unfinished"/>
+ <translation>購読済みのものがありません。チャンネルを購読するには星マークをクリックします。</translation>
</message>
</context>
<context>
<message>
<location filename="src/downloadmanager.cpp" line="76"/>
<source>This is just the demo version of %1.</source>
- <translation>これは %1 の試用版です。</translation>
+ <translation>これは%1 の試用版です。</translation>
</message>
<message>
<location filename="src/downloadmanager.cpp" line="78"/>
<source>It can only download videos shorter than %1 minutes so you can test the download functionality.</source>
- <translation type="unfinished"/>
+ <translation>動画を%1分程度でダウンロードできる機能を試すことができます。</translation>
</message>
<message>
<location filename="src/downloadmanager.cpp" line="84"/>
<message>
<location filename="src/downloadmanager.cpp" line="85"/>
<source>Get the full version</source>
- <translation>製品版を入手する。</translation>
+ <translation>製品版を入手する</translation>
</message>
<message>
<location filename="src/downloadmanager.cpp" line="156"/>
<source>%1 downloaded in %2</source>
- <translation type="unfinished"/>
+ <translation>%1を%2にダウンロード中</translation>
</message>
<message>
<location filename="src/downloadmanager.cpp" line="159"/>
<source>Download finished</source>
- <translation type="unfinished"/>
+ <translation>ダウンロード完了</translation>
</message>
<message numerus="yes">
<location filename="src/downloadmanager.cpp" line="164"/>
<source>%n Download(s)</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n件のダウンロード</numerusform></translation>
</message>
</context>
<context>
<message>
<location filename="src/downloadsettings.cpp" line="78"/>
<source>Choose the download location</source>
- <translation type="unfinished"/>
+ <translation>ダウンロード場所を選択</translation>
</message>
<message>
<location filename="src/downloadsettings.cpp" line="90"/>
<source>Download location changed.</source>
- <translation type="unfinished"/>
+ <translation>ダウンロード場所を変更しました。</translation>
</message>
<message>
<location filename="src/downloadsettings.cpp" line="92"/>
<source>Current downloads will still go in the previous location.</source>
- <translation type="unfinished"/>
+ <translation>現在ダウンロードしているファイルは移動されます。</translation>
</message>
<message>
<location filename="src/downloadsettings.cpp" line="107"/>
<source>Downloading to: %1</source>
- <translation type="unfinished"/>
+ <translation>%1にダウンロード中</translation>
</message>
</context>
<context>
<message>
<location filename="local/src/updatedialog.cpp" line="60"/>
<source>Downloading update...</source>
- <translation type="unfinished"/>
+ <translation>アップデートをダウンロード中...</translation>
</message>
</context>
<context>
<message>
<location filename="src/homeview.cpp" line="60"/>
<source>Find videos and channels by keyword</source>
- <translation type="unfinished"/>
+ <translation>キーワードで動画とチャンネルを検索します</translation>
</message>
<message>
<location filename="src/homeview.cpp" line="65"/>
<source>Browse</source>
- <translation type="unfinished"/>
+ <translation>カテゴリ</translation>
</message>
<message>
<location filename="src/homeview.cpp" line="67"/>
<source>Browse videos by category</source>
- <translation type="unfinished"/>
+ <translation>カテゴリ別に動画を参照します</translation>
</message>
<message>
<location filename="src/homeview.cpp" line="71"/>
<source>Subscriptions</source>
- <translation type="unfinished"/>
+ <translation>購読</translation>
</message>
<message>
<location filename="src/homeview.cpp" line="73"/>
<source>Channel subscriptions</source>
- <translation type="unfinished"/>
+ <translation>購読したチャンネルを視聴します</translation>
</message>
<message>
<location filename="src/homeview.h" line="45"/>
<message>
<location filename="src/mainwindow.cpp" line="267"/>
<source>&Stop</source>
- <translation>ストップ(&S)</translation>
+ <translation>再生停止(&S)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="268"/>
<source>Stop playback and go back to the search view</source>
- <translation>å\86\8dç\94\9fã\82\92å\81\9cæ¢ã\81\95ã\81\9bã\81¦ã\80\81æ¤\9cç´¢ã\83\93ã\83¥ã\83¼に戻ります</translation>
+ <translation>å\86\8dç\94\9fã\82\92å\81\9cæ¢ã\81\97ã\80\81æ¤\9cç´¢ç\94»é\9d¢に戻ります</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="283"/>
<source>S&kip</source>
- <translation>スキップ(&k)</translation>
+ <translation>次へ(&k)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="284"/>
<source>Skip to the next video</source>
- <translation>次の動画へ</translation>
+ <translation>次の動画へ進みます</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="290"/>
<message>
<location filename="src/mainwindow.cpp" line="297"/>
<source>&Full Screen</source>
- <translation>フルスクリーン(&F)</translation>
+ <translation>フルスクリーンモード(&F)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="298"/>
<source>Go full screen</source>
- <translation>フルスクリーン</translation>
+ <translation>フルスクリーンモードで再生します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="312"/>
<source>Hide the playlist and the toolbar</source>
- <translation>プレイリストとツールバーを隠す</translation>
+ <translation>ã\83\97ã\83¬ã\82¤ã\83ªã\82¹ã\83\88ã\81¨ã\83\84ã\83¼ã\83«ã\83\90ã\83¼ã\82\92é\9a ã\81\97ã\81¾ã\81\99</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="325"/>
<message>
<location filename="src/mainwindow.cpp" line="332"/>
<source>Copy the current video YouTube link to the clipboard</source>
- <translation>このビデオのYouTubeページへのリンクをクリップボードにコピーします</translation>
+ <translation>この動画のYouTubeページへのリンクをコピーします</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="339"/>
<source>Copy the current video stream URL to the clipboard</source>
- <translation type="unfinished"/>
+ <translation>現在の動画のURLをコピーします</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="346"/>
<source>Find other video parts hopefully in the right order</source>
- <translation type="unfinished"/>
+ <translation>この動画の他のパートを検索します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="352"/>
<message>
<location filename="src/mainwindow.cpp" line="353"/>
<source>Remove the selected videos from the playlist</source>
- <translation>プレイリストから選択した動画を削除</translation>
+ <translation>プレイリストから選択した動画を削除します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="359"/>
<message>
<location filename="src/mainwindow.cpp" line="378"/>
<source>Clear the search history. Cannot be undone.</source>
- <translation>検索履歴を削除します。取り消しは出来ません。</translation>
+ <translation>検索履歴を消去します。取り消しは出来ません。</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="383"/>
<message>
<location filename="src/mainwindow.cpp" line="386"/>
<source>Bye</source>
- <translation>プログラムを終了</translation>
+ <translation>Minitubeを終了します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="390"/>
<source>&Website</source>
- <translation>&Webページへ</translation>
+ <translation>ウェブサイト(&W)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="392"/>
<source>%1 on the Web</source>
- <translation>%1のWebページを開きます</translation>
+ <translation>%1のウェブサイトを開きます</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="398"/>
<source>Please support the continued development of %1</source>
- <translation>%1の開発をサポートしてください!</translation>
+ <translation>寄付して%1の開発を助けます</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="403"/>
<source>&About</source>
- <translation>プログラムについて(&A)</translation>
+ <translation>Minitubeについて(&A)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="405"/>
<message>
<location filename="src/mainwindow.cpp" line="413"/>
<source>Search</source>
- <translation>検索</translation>
+ <translation>動画を検索します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="432"/>
<source>Mute volume</source>
- <translation>ã\83\9fã\83¥ã\83¼ã\83\88ã\81«ã\81\99ã\82\8b</translation>
+ <translation>ã\83\9fã\83¥ã\83¼ã\83\88ã\81«ã\81\97ã\81¾ã\81\99</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="466"/>
<message>
<location filename="src/mainwindow.cpp" line="467"/>
<source>Show details about video downloads</source>
- <translation type="unfinished"/>
+ <translation>動画のダウンロードについての詳細を表示</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="475"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>スナップショットを撮る(&S)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<source>&Subscribe to Channel</source>
- <translation type="unfinished"/>
+ <translation>チャンネル購読(&S)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="501"/>
<source>Share the current video using %1</source>
- <translation type="unfinished"/>
+ <translation>現在の動画を%1で共有します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="521"/>
<message>
<location filename="src/mainwindow.cpp" line="537"/>
<source>&Float on Top</source>
- <translation type="unfinished"/>
+ <translation>最前面に表示(&F)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="542"/>
<source>&Stop After This Video</source>
- <translation type="unfinished"/>
+ <translation>再生終了後に停止(&S)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="549"/>
<source>&Report an Issue...</source>
- <translation>問題点を報告(&R)...</translation>
+ <translation>問題点を報告する(&R)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="553"/>
<source>&Refine Search...</source>
- <translation type="unfinished"/>
+ <translation>絞り込み検索(&R)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="565"/>
<message>
<location filename="src/mainwindow.cpp" line="568"/>
<source>&Related Videos</source>
- <translation type="unfinished"/>
+ <translation>関連動画(&R)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="570"/>
<source>Watch videos related to the current one</source>
- <translation type="unfinished"/>
+ <translation>関連動画を検索します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="576"/>
<source>Open in &Browser...</source>
- <translation type="unfinished"/>
+ <translation>ブラウザで見る(&B)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>%1を評価(&L)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/mainwindow.cpp" line="589"/>
<source>Buy %1...</source>
- <translation>%1 を購入...</translation>
+ <translation>%1を購入する</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="619"/>
<message>
<location filename="src/mainwindow.cpp" line="760"/>
<source>Press %1 to raise the volume, %2 to lower it</source>
- <translation>é\9f³é\87\8fã\82\92ä¸\8aã\81\92ã\82\8bã\81«ã\81¯%1ã\82\92ã\80\81ä¸\8bã\81\92ã\82\8bã\81«ã\81¯%2ã\82\92æ\8a¼ã\81\97ã\81¦ã\81\8fã\81 ã\81\95ã\81\84</translation>
+ <translation>é\9f³é\87\8fã\82\92ä¸\8aã\81\92ã\82\8bã\81«ã\81¯%1ã\82\92ã\80\81ä¸\8bã\81\92ã\82\8bã\81«ã\81¯%2ã\82\92æ\8a¼ã\81\97ã\81¾ã\81\99</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="955"/>
<message>
<location filename="src/mainwindow.cpp" line="1006"/>
<source>Do you want to exit %1 with a download in progress?</source>
- <translation type="unfinished"/>
+ <translation>進行中の%1のダウンロードを停止しますか?</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1007"/>
<source>If you close %1 now, this download will be cancelled.</source>
- <translation type="unfinished"/>
+ <translation>あなたは今%1を停止した場合は、このダウンロードはキャンセルされます。</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1012"/>
<message>
<location filename="src/mainwindow.cpp" line="1013"/>
<source>Wait for download to finish</source>
- <translation type="unfinished"/>
+ <translation>ダウンロードが完了するまでお待ちください</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1174"/>
<message>
<location filename="src/mainwindow.cpp" line="1601"/>
<source>%1 version %2 is now available.</source>
- <translation>%1 バージョン %2 が利用可能です</translation>
+ <translation>%1 バージョン %2 が利用可能です。</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1605"/>
<message>
<location filename="src/mainwindow.cpp" line="277"/>
<source>Go back to the previous track</source>
- <translation>前の曲へ飛びます</translation>
+ <translation>前の動画へ戻ります</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="311"/>
<source>&Compact Mode</source>
- <translation>コンパクト モード(&C)</translation>
+ <translation>コンパクトモード(&C)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="324"/>
<source>Open the &YouTube Page</source>
- <translation>YouTube のページを開く(&Y)</translation>
+ <translation>YouTubeのページを開く(&Y)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="331"/>
<source>Copy the YouTube &Link</source>
- <translation>YouTube のリンクをコピー(&L)</translation>
+ <translation>YouTubeへのリンクをコピー(&L)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="338"/>
<source>Copy the Video Stream &URL</source>
- <translation type="unfinished"/>
+ <translation>動画のURLをコピー(&U)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="345"/>
<source>Find Video &Parts</source>
- <translation type="unfinished"/>
+ <translation>他のパートを探す(&P)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="373"/>
<source>&Clear Recent Searches</source>
- <translation>æ\9c\80è¿\91ã\81®æ¤\9cç´¢を消去(&C)</translation>
+ <translation>æ¤\9c索履æ´を消去(&C)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="397"/>
<message>
<location filename="src/mainwindow.cpp" line="459"/>
<source>&Manually Start Playing</source>
- <translation type="unfinished"/>
+ <translation>手動で再生(&M)</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="460"/>
<source>Manually start playing videos</source>
- <translation type="unfinished"/>
+ <translation>動画の自動再生をしない</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="797"/>
<source>Choose your content location</source>
- <translation type="unfinished"/>
+ <translation>コンテンツの地域を選択</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1073"/>
<message>
<location filename="src/mainwindow.cpp" line="1074"/>
<source>Resume playback</source>
- <translation>再生再開します</translation>
+ <translation>再生を再開します</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1333"/>
<message>
<location filename="src/mainwindow.cpp" line="1425"/>
<source>Volume is muted</source>
- <translation>音量OFFにしました</translation>
+ <translation>ミュートにしました</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1428"/>
<source>Volume is unmuted</source>
- <translation>音量ONにしました</translation>
+ <translation>ミュートを解除しました</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1435"/>
<source>Maximum video definition set to %1</source>
- <translation type="unfinished"/>
+ <translation>最大画質を%1に設定する</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="1476"/>
<message>
<location filename="src/mediaview.cpp" line="690"/>
<source>You can now paste the video stream URL into another application</source>
- <translation type="unfinished"/>
+ <translation>他のアプリケーションに動画のURLを貼り付けることができます</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="691"/>
<source>The link will be valid only for a limited time.</source>
- <translation type="unfinished"/>
+ <translation>リンクは制限時間内のみ有効です。</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="780"/>
<source>This is just the demo version of %1.</source>
- <translation>これは %1 の試用版です。</translation>
+ <translation>これは%1 の試用版です。</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="781"/>
<location filename="src/mediaview.cpp" line="946"/>
<source>of</source>
<comment>Used in video parts, as in '2 of 3'</comment>
- <translation type="unfinished"/>
+ <translation>から</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="959"/>
<source>part</source>
<comment>This is for video parts, as in 'Cool video - part 1'</comment>
- <translation type="unfinished"/>
+ <translation>パート</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="961"/>
<source>episode</source>
<comment>This is for video parts, as in 'Cool series - episode 1'</comment>
- <translation type="unfinished"/>
+ <translation>エピソード</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="1074"/>
<source>Sent from %1</source>
- <translation type="unfinished"/>
+ <translation>%1に送る</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="1109"/>
<source>Unsubscribe from %1</source>
- <translation type="unfinished"/>
+ <translation>%1の購読を解除する</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="1113"/>
<source>Subscribe to %1</source>
- <translation type="unfinished"/>
+ <translation>%1を購読する</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="788"/>
<source>Get the full version</source>
- <translation>製品版を入手する。</translation>
+ <translation>製品版を入手する</translation>
</message>
<message>
<location filename="src/mediaview.cpp" line="827"/>
<source>Downloading %1</source>
- <translation type="unfinished"/>
+ <translation>%1をダウンロード中</translation>
</message>
</context>
<context>
<message>
<location filename="local/src/updatedialog.cpp" line="21"/>
<source>A new version of %1 is available!</source>
- <translation type="unfinished"/>
+ <translation>新しいバージョンの%1があります!</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="28"/>
<source>%1 %2 is now available. You have %3.</source>
- <translation type="unfinished"/>
+ <translation>%1 %2があります。あなたは%3です。</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="33"/>
<source>Would you like to download it now?</source>
- <translation type="unfinished"/>
+ <translation>今すぐダウンロードしますか?</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="39"/>
<source>Skip This Version</source>
- <translation type="unfinished"/>
+ <translation>このバージョンをスキップ</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="43"/>
<source>Remind Me Later</source>
- <translation>このバージョンをスキップ</translation>
+ <translation>後で通知</translation>
</message>
<message>
<location filename="local/src/updatedialog.cpp" line="47"/>
<source>Install Update</source>
- <translation type="unfinished"/>
+ <translation>アップデートする</translation>
</message>
</context>
<context>
<message>
<location filename="src/playlistitemdelegate.cpp" line="332"/>
<source>%1 of %2 (%3) — %4</source>
- <translation type="unfinished"/>
+ <translation>%1は%2(%3)%4</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="339"/>
<source>Preparing</source>
- <translation type="unfinished"/>
+ <translation>準備中</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="341"/>
<message>
<location filename="src/playlistitemdelegate.cpp" line="345"/>
<source>Stopped</source>
- <translation type="unfinished"/>
+ <translation>停止</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="381"/>
<message>
<location filename="src/playlistitemdelegate.cpp" line="391"/>
<source>Show in %1</source>
- <translation type="unfinished"/>
+ <translation>%1を表示</translation>
</message>
<message>
<location filename="src/playlistitemdelegate.cpp" line="393"/>
<message>
<location filename="src/playlistitemdelegate.cpp" line="402"/>
<source>Restart downloading</source>
- <translation type="unfinished"/>
+ <translation>ダウンロード再開</translation>
</message>
</context>
<context>
<message>
<location filename="src/playlistmodel.cpp" line="73"/>
<source>Show %1 More</source>
- <translation>さらに%1エントリ観る</translation>
+ <translation>さらに%1件表示</translation>
</message>
<message>
<location filename="src/playlistmodel.cpp" line="74"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="55"/>
<source>Relevance</source>
- <translation type="unfinished"/>
+ <translation>関連性</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="56"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="58"/>
<source>Rating</source>
- <translation type="unfinished"/>
+ <translation>評価</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="76"/>
<source>Anytime</source>
- <translation type="unfinished"/>
+ <translation>すべて</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="77"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="78"/>
<source>7 Days</source>
- <translation>7日間</translation>
+ <translation>一週間以内</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="79"/>
<source>30 Days</source>
- <translation>30日間</translation>
+ <translation>1ヶ月以内</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="93"/>
<source>Duration</source>
- <translation type="unfinished"/>
+ <translation>再生時間</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="97"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="98"/>
<source>Short</source>
- <translation type="unfinished"/>
+ <translation>短い</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="99"/>
<source>Medium</source>
- <translation type="unfinished"/>
+ <translation>普通</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="100"/>
<source>Long</source>
- <translation type="unfinished"/>
+ <translation>長い</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="103"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="120"/>
<source>Quality</source>
- <translation>品質</translation>
+ <translation>画質</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="125"/>
<source>High Definition</source>
- <translation type="unfinished"/>
+ <translation>高画質</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="128"/>
<source>720p or higher</source>
- <translation type="unfinished"/>
+ <translation>720pか高画質</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="142"/>
<message>
<location filename="src/searchview.cpp" line="88"/>
<source>Welcome to <a href='%1'>%2</a>,</source>
- <translation>ようこそ<a href='%1'>%2</a>へ!</translation>
+ <translation>ようこそ<a href='%1'>%2</a>へ</translation>
</message>
<message>
<location filename="src/searchview.cpp" line="195"/>
<source>Get the full version</source>
- <translation>製品版を入手する。</translation>
+ <translation>製品版を入手する</translation>
</message>
<message>
<location filename="src/searchview.cpp" line="114"/>
<source>Enter</source>
<extracomment>"Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"</extracomment>
- <translation type="unfinished"/>
+ <translation>キーワードを入力して</translation>
</message>
<message>
<location filename="src/searchview.cpp" line="119"/>
<source>a keyword</source>
- <translation type="unfinished"/>
+ <translation>動画</translation>
</message>
<message>
<location filename="src/searchview.cpp" line="120"/>
<source>a channel</source>
- <translation type="unfinished"/>
+ <translation>チャンネル</translation>
</message>
<message>
<location filename="src/searchview.cpp" line="125"/>
<source>to start watching videos.</source>
- <translation type="unfinished"/>
+ <translation>を検索する。</translation>
</message>
<message>
<location filename="src/searchview.cpp" line="149"/>
<message>
<location filename="src/sidebarwidget.cpp" line="68"/>
<source>Refine Search</source>
- <translation type="unfinished"/>
+ <translation>絞り込み検索</translation>
</message>
<message>
<location filename="src/sidebarwidget.cpp" line="163"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>%1にスナップショットを保存しました</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>スナップショットの場所を変更しました。</translation>
</message>
</context>
<context>
<message>
<location filename="src/standardfeedsview.cpp" line="105"/>
<source>Most Popular</source>
- <translation type="unfinished"/>
+ <translation>とても人気</translation>
</message>
</context>
<context>
<message>
<location filename="src/video.cpp" line="309"/>
<source>Cannot get video stream for %1</source>
- <translation type="unfinished"/>
+ <translation>%1の動画を取得できませんでした</translation>
</message>
<message>
<location filename="src/video.cpp" line="326"/>
--- /dev/null
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="ko_KR" version="2.0">
+<defaultcodec>UTF-8</defaultcodec>
+<context>
+ <name>AboutView</name>
+ <message>
+ <location filename="src/aboutview.cpp" line="52"/>
+ <source>There's life outside the browser!</source>
+ <translation>브라우저 바깥에 생활이 존재 합니다!</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="53"/>
+ <source>Version %1</source>
+ <translation>버전 %1</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="58"/>
+ <source>Licensed to: %1</source>
+ <translation>라이센스 소유자: %1</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="62"/>
+ <source>%1 is Free Software but its development takes precious time.</source>
+ <translation>%1는 무료 프로그램이지만 개발에는 시간과 노력이 소요 됩니다.</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="63"/>
+ <source>Please <a href='%1'>donate</a> to support the continued development of %2.</source>
+ <translation>%2의 계속된 개발을 위해 <a href='%1'>기부</a>룰 해주요...</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="67"/>
+ <source>You may want to try my other apps as well:</source>
+ <translation>저희의 다른 프로그램도 사용해 보세요"</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="70"/>
+ <source>%1, a YouTube music player</source>
+ <translation>%1, 유튜브 음악 재생기.</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="74"/>
+ <source>%1, a music player</source>
+ <translation>%1 음악 재생기</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="80"/>
+ <source>Translate %1 to your native language using %2</source>
+ <translation>%2을(를) 사용해서 %1를 사용자의 언어로 번역 하세요.</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="85"/>
+ <source>Icon designed by %1.</source>
+ <translation>아이콘 디자인: %1</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="89"/>
+ <source>Released under the <a href='%1'>GNU General Public License</a></source>
+ <translation><a href='%1'>GNU General Public License</a>하에 배포 됩니다.</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="104"/>
+ <source>&Close</source>
+ <translation>닫기(&C)</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.h" line="40"/>
+ <source>About</source>
+ <translation>프로그램 정보</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.h" line="42"/>
+ <source>What you always wanted to know about %1 and never dared to ask</source>
+ <translation>%1에 대해 궁금한게 있으면 언제든 문의 하세요.</translation>
+ </message>
+</context>
+<context>
+ <name>ActivationDialog</name>
+ <message>
+ <location filename="local/src/activationdialog.cpp" line="17"/>
+ <source>Enter your License Details</source>
+ <translation>라이센스 정보 입력</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationdialog.cpp" line="29"/>
+ <source>&Email:</source>
+ <translation>Email(&E)</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationdialog.cpp" line="35"/>
+ <source>&Code:</source>
+ <translation>코드(&C)</translation>
+ </message>
+</context>
+<context>
+ <name>ActivationView</name>
+ <message>
+ <location filename="local/src/activationview.cpp" line="47"/>
+ <source>Please license %1</source>
+ <translation>%1을(를) 구입 하세요</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="51"/>
+ <source>This demo has expired.</source>
+ <translation>데모 만료!</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="53"/>
+ <source>The full version allows you to watch videos without interruptions.</source>
+ <oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
+ <translation>구입 버전은 방해받지 않고 비디오를 감상 할수 있습니다.</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="55"/>
+ <source>Without a license, the application will expire in %1 days.</source>
+ <translation>라이센스를 구입하지 않으면, %1일후 만료 됩니다.</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="57"/>
+ <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
+ <translation>구입하면, 제가 %1를 만드는데 드는 노력을 지원 합니다.</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="70"/>
+ <source>Use Demo</source>
+ <translation>데모 사용</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="77"/>
+ <source>Enter License</source>
+ <translation>라이센스 입력</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="85"/>
+ <source>Buy License</source>
+ <translation>라이센스 구입</translation>
+ </message>
+</context>
+<context>
+ <name>ChannelAggregator</name>
+ <message>
+ <location filename="src/channelaggregator.cpp" line="131"/>
+ <source>By %1</source>
+ <translation>By %1</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="src/channelaggregator.cpp" line="133"/>
+ <source>You have %n new video(s)</source>
+ <translation type="unfinished"><numerusform></numerusform></translation>
+ </message>
+</context>
+<context>
+ <name>ChannelItemDelegate</name>
+ <message>
+ <location filename="src/channelitemdelegate.cpp" line="66"/>
+ <source>All Videos</source>
+ <translation>모든 비디오</translation>
+ </message>
+ <message>
+ <location filename="src/channelitemdelegate.cpp" line="83"/>
+ <source>Unwatched Videos</source>
+ <translation>안 본 비디오</translation>
+ </message>
+</context>
+<context>
+ <name>ChannelView</name>
+ <message>
+ <location filename="src/channelview.cpp" line="151"/>
+ <source>Mark all as watched</source>
+ <translation>모두 본 걸로 표시</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="159"/>
+ <source>Show Updated</source>
+ <translation>업데이트 표시</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="103"/>
+ <source>Name</source>
+ <translation>이름</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="110"/>
+ <source>Last Updated</source>
+ <translation>마지막 업데이트</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="117"/>
+ <source>Last Added</source>
+ <translation>마지막 추가</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="124"/>
+ <source>Last Watched</source>
+ <translation>마지막 시청</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="131"/>
+ <source>Most Watched</source>
+ <translation>가장 많이 시청</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="139"/>
+ <source>Sort by</source>
+ <translation>정렬</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="221"/>
+ <source>All Videos</source>
+ <translation>모든 비디오</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="225"/>
+ <source>Unwatched Videos</source>
+ <translation>안 본 비디오</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="243"/>
+ <source>Mark as Watched</source>
+ <translation>본 비디오로 마크</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="256"/>
+ <source>Unsubscribe</source>
+ <translation>구독 취소</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="269"/>
+ <source>There are no updated subscriptions at this time.</source>
+ <translation>현재 업데이트된 구독이 없습니다.</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="271"/>
+ <source>You have no subscriptions. Use the star symbol to subscribe to channels.</source>
+ <translation>구독 항목이 없습니다. 채널을 구독 하려면 별 아이콘을 사용 하세요.</translation>
+ </message>
+</context>
+<context>
+ <name>ClearButton</name>
+ <message>
+ <location filename="src/searchlineedit.cpp" line="56"/>
+ <source>Clear</source>
+ <translation>지우기</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadItem</name>
+ <message>
+ <location filename="src/downloaditem.cpp" line="406"/>
+ <source>bytes</source>
+ <translation>바이트</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="409"/>
+ <source>KB</source>
+ <translation>KB</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="412"/>
+ <source>MB</source>
+ <translation>MB</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="421"/>
+ <source>bytes/sec</source>
+ <translation>바이트/초</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="424"/>
+ <source>KB/sec</source>
+ <translation>KB/초</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="427"/>
+ <source>MB/sec</source>
+ <translation>MB/초</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="433"/>
+ <source>seconds</source>
+ <translation>초</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="436"/>
+ <source>minutes</source>
+ <translation>분</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="439"/>
+ <source>%4 %5 remaining</source>
+ <translation>%4 %5 남음</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadManager</name>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="76"/>
+ <source>This is just the demo version of %1.</source>
+ <translation>%1의 데모 버전 입니다.</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="78"/>
+ <source>It can only download videos shorter than %1 minutes so you can test the download functionality.</source>
+ <translation>사용자가 다운로드 기능을 테스트 할수 있도록 %1보다 짧은 비디오만 다운로드 됩니다.</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="84"/>
+ <source>Continue</source>
+ <translation>계속</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="85"/>
+ <source>Get the full version</source>
+ <translation>풀 버전 구입</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="156"/>
+ <source>%1 downloaded in %2</source>
+ <translation>%1 downloaded in %2</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="159"/>
+ <source>Download finished</source>
+ <translation>다운로드 완료</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="src/downloadmanager.cpp" line="164"/>
+ <source>%n Download(s)</source>
+ <translation type="unfinished"><numerusform></numerusform></translation>
+ </message>
+</context>
+<context>
+ <name>DownloadSettings</name>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="35"/>
+ <source>Change location...</source>
+ <translation>위치 변경...</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="78"/>
+ <source>Choose the download location</source>
+ <translation>다운로드 위치 선택</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="90"/>
+ <source>Download location changed.</source>
+ <translation>다운로드 위치 변경됨</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="92"/>
+ <source>Current downloads will still go in the previous location.</source>
+ <translation>현재 다운로드는 이전 위치에 저장 됩니다.</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="107"/>
+ <source>Downloading to: %1</source>
+ <translation>다운로드 위치: %1</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadView</name>
+ <message>
+ <location filename="src/downloadview.cpp" line="38"/>
+ <location filename="src/downloadview.h" line="45"/>
+ <source>Downloads</source>
+ <translation>다운로드</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadWidget</name>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="60"/>
+ <source>Downloading update...</source>
+ <translation>업데이트 다운로드...</translation>
+ </message>
+</context>
+<context>
+ <name>GlobalShortcuts</name>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="36"/>
+ <source>Play</source>
+ <translation>재생</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="37"/>
+ <source>Pause</source>
+ <translation>일시정지</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="38"/>
+ <source>Play/Pause</source>
+ <translation>재생/일시정지</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="39"/>
+ <source>Stop</source>
+ <translation>정지</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="40"/>
+ <source>Stop playing after current track</source>
+ <translation>현재 트랙 이후 정지</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="41"/>
+ <source>Next track</source>
+ <translation>다음 트랙</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="42"/>
+ <source>Previous track</source>
+ <translation>이전 트랙</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="43"/>
+ <source>Increase volume</source>
+ <translation>볼륨 키우기</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="44"/>
+ <source>Decrease volume</source>
+ <translation>볼륨 줄이기</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="45"/>
+ <source>Mute</source>
+ <translation>무음</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="46"/>
+ <source>Seek forward</source>
+ <translation>앞으로찾기</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="47"/>
+ <source>Seek backward</source>
+ <translation>뒤로찾기</translation>
+ </message>
+</context>
+<context>
+ <name>HomeView</name>
+ <message>
+ <location filename="src/homeview.cpp" line="58"/>
+ <source>Search</source>
+ <translation>검색</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="60"/>
+ <source>Find videos and channels by keyword</source>
+ <translation>키워드로 비디오 채널 검색</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="65"/>
+ <source>Browse</source>
+ <translation>탐색</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="67"/>
+ <source>Browse videos by category</source>
+ <translation>카테고리로 비디오 탐색</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="71"/>
+ <source>Subscriptions</source>
+ <translation>구독</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="73"/>
+ <source>Channel subscriptions</source>
+ <translation>채널 구독</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.h" line="45"/>
+ <source>Make yourself comfortable</source>
+ <translation>편안하게 하세요.</translation>
+ </message>
+</context>
+<context>
+ <name>LoadingWidget</name>
+ <message>
+ <location filename="src/loadingwidget.cpp" line="114"/>
+ <source>Error</source>
+ <translation>에러</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="src/mainwindow.cpp" line="267"/>
+ <source>&Stop</source>
+ <translation>정지(&S)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="268"/>
+ <source>Stop playback and go back to the search view</source>
+ <translation>재생을 멈추고 검색 보기로 이동</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="283"/>
+ <source>S&kip</source>
+ <translation>건너뛰기(&K)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="284"/>
+ <source>Skip to the next video</source>
+ <translation>다음 비디오로 건너뛰기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="290"/>
+ <location filename="src/mainwindow.cpp" line="1060"/>
+ <source>&Pause</source>
+ <translation>일시정지(&P)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="291"/>
+ <location filename="src/mainwindow.cpp" line="1061"/>
+ <source>Pause playback</source>
+ <translation>재생 일시정지</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="297"/>
+ <source>&Full Screen</source>
+ <translation>전체 화면(&F)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="298"/>
+ <source>Go full screen</source>
+ <translation>전체화면 보기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="312"/>
+ <source>Hide the playlist and the toolbar</source>
+ <translation>도구 모음과 재생 목록 숨기기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="325"/>
+ <source>Go to the YouTube video page and pause playback</source>
+ <translation>재생을 멈추고 YouTube 비디오페이지로 이동</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="332"/>
+ <source>Copy the current video YouTube link to the clipboard</source>
+ <translation>현재 비디오의 YouTube링크를 클립보드에 복사</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="339"/>
+ <source>Copy the current video stream URL to the clipboard</source>
+ <translation>현재 비디오 스트림 URL을 클립보드에 복사</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="346"/>
+ <source>Find other video parts hopefully in the right order</source>
+ <translation>연결된 다른 비디오 조각을 가능한한 올바른 순서로 찾기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="352"/>
+ <source>&Remove</source>
+ <translation>제거(&R)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="353"/>
+ <source>Remove the selected videos from the playlist</source>
+ <translation>재생 목록에서 현재 비디오 제거</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="359"/>
+ <source>Move &Up</source>
+ <translation>위로 이동(&U)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="360"/>
+ <source>Move up the selected videos in the playlist</source>
+ <translation>재생 목록에서 선택된 비디오의 위 항목으로 이동</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="366"/>
+ <source>Move &Down</source>
+ <translation>아래로이동(&D)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="367"/>
+ <source>Move down the selected videos in the playlist</source>
+ <translation>재생 목록에서 선택된 비디오의 아래 항목으로 이동</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="378"/>
+ <source>Clear the search history. Cannot be undone.</source>
+ <translation>검색했던 목록 지우기. 되돌릴수 없음.</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="383"/>
+ <source>&Quit</source>
+ <translation>종료(&Q)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="386"/>
+ <source>Bye</source>
+ <translation>안녕...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="390"/>
+ <source>&Website</source>
+ <translation>웹사이트(&W)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="392"/>
+ <source>%1 on the Web</source>
+ <translation>%1의 웹사이트</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="398"/>
+ <source>Please support the continued development of %1</source>
+ <translation>%1의 계속되는 개발을 도와 주세요.</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="403"/>
+ <source>&About</source>
+ <translation>프로그램 정보(&A)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="405"/>
+ <source>Info about %1</source>
+ <translation>%1의 정보</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="413"/>
+ <source>Search</source>
+ <translation>검색</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="432"/>
+ <source>Mute volume</source>
+ <translation>볼륨 소거</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="466"/>
+ <location filename="src/mainwindow.cpp" line="1490"/>
+ <source>&Downloads</source>
+ <translation>다운로드(&D)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="467"/>
+ <source>Show details about video downloads</source>
+ <translation>비디오 다운로드에 대한 자세한 정보 표시</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="475"/>
+ <source>&Download</source>
+ <translation>다운로드(&D)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="476"/>
+ <source>Download the current video</source>
+ <translation>현재 비디오 다운로드</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="486"/>
+ <source>Take &Snapshot</source>
+ <translation>스냅샷 찍기(&S)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="493"/>
+ <source>&Subscribe to Channel</source>
+ <translation>채널 구독(&S)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="501"/>
+ <source>Share the current video using %1</source>
+ <translation>%1을(를) 사용해서 현재 비디오 공유</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="521"/>
+ <source>&Email</source>
+ <translation>Email(&E)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="522"/>
+ <source>Email</source>
+ <translation>Email</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="527"/>
+ <source>&Close</source>
+ <translation>닫기(&C)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="537"/>
+ <source>&Float on Top</source>
+ <translation>위에 떠있기(&F)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="542"/>
+ <source>&Stop After This Video</source>
+ <translation>이 비디오 재생 후 정지(&S)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="549"/>
+ <source>&Report an Issue...</source>
+ <translation>문제점 보고...(&R)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="553"/>
+ <source>&Refine Search...</source>
+ <translation>검색 재정의(&R)...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="565"/>
+ <source>More...</source>
+ <translation>추가정보...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="568"/>
+ <source>&Related Videos</source>
+ <translation>관련된 비디오...(&R)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="570"/>
+ <source>Watch videos related to the current one</source>
+ <translation>현재 비디오와 관련된 비디오 보기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="576"/>
+ <source>Open in &Browser...</source>
+ <translation>브라우저에서 열기(&B)...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="583"/>
+ <source>&Love %1? Rate it!</source>
+ <translation>%1를 좋아하시나요? 평점을 주세요!</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="605"/>
+ <source>&Application</source>
+ <translation>어플리케이션(&A)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="589"/>
+ <source>Buy %1...</source>
+ <translation>%1 구입...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="619"/>
+ <source>&Playback</source>
+ <translation>재생(&P)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="633"/>
+ <source>&Playlist</source>
+ <translation>재생 목록(&P)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="642"/>
+ <source>&Video</source>
+ <translation>비디오(&V)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="658"/>
+ <source>&View</source>
+ <translation>보기(&V)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="665"/>
+ <source>&Share</source>
+ <translation>공유(&S)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="679"/>
+ <source>&Help</source>
+ <translation>도움말(&H)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="760"/>
+ <source>Press %1 to raise the volume, %2 to lower it</source>
+ <translation>%1을(를) 눌러 볼륨 증가 , %2을(를) 눌러 볼륨 감소</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="955"/>
+ <location filename="src/mainwindow.cpp" line="961"/>
+ <source>Opening %1</source>
+ <translation>%1 열기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1006"/>
+ <source>Do you want to exit %1 with a download in progress?</source>
+ <translation>다운로드가 진행 중인데 %1을(를) 종료 할까요?</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1007"/>
+ <source>If you close %1 now, this download will be cancelled.</source>
+ <translation>%1를 지금 종료하면, 다운로드는 취소 됩니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1012"/>
+ <source>Close and cancel download</source>
+ <translation>다운로드 취소하고 종료</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1013"/>
+ <source>Wait for download to finish</source>
+ <translation>다운로드 끝날때 까지 대기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1174"/>
+ <source>Leave &Full Screen</source>
+ <translation>전체 화면 나가기(&F)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1601"/>
+ <source>%1 version %2 is now available.</source>
+ <translation>%1 version %2이(가) 사용 가능 합니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1605"/>
+ <source>Remind me later</source>
+ <translation>다음에 알리기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1606"/>
+ <source>Update</source>
+ <translation>업데이트</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1053"/>
+ <source>Error: %1</source>
+ <translation>에러: %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="276"/>
+ <source>P&revious</source>
+ <translation>이전(&R)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="277"/>
+ <source>Go back to the previous track</source>
+ <translation>이전트랙으로 이동</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="311"/>
+ <source>&Compact Mode</source>
+ <translation>컴팩트 모드(&C)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="324"/>
+ <source>Open the &YouTube Page</source>
+ <translation>YouTube 페이지 열기(&Y)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="331"/>
+ <source>Copy the YouTube &Link</source>
+ <translation>YouTube 링크 복사(&L)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="338"/>
+ <source>Copy the Video Stream &URL</source>
+ <translation>비디오 스트림 URL을 복사(&U)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="345"/>
+ <source>Find Video &Parts</source>
+ <translation>비디오 부분 찾기(&P)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="373"/>
+ <source>&Clear Recent Searches</source>
+ <translation>최근 검색 지우기(&C)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="397"/>
+ <source>Make a &Donation</source>
+ <translation>기부하기(&D)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="459"/>
+ <source>&Manually Start Playing</source>
+ <translation>수동으로 재생 시작(&M)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="460"/>
+ <source>Manually start playing videos</source>
+ <translation>수동으로 비디오 재생 시작 하기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="797"/>
+ <source>Choose your content location</source>
+ <translation>콘텐츠 위치 선택</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1073"/>
+ <source>&Play</source>
+ <translation>재생(&P)</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1074"/>
+ <source>Resume playback</source>
+ <translation>재생 이어하기</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1333"/>
+ <source>Remaining time: %1</source>
+ <translation>남은 시간: %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1419"/>
+ <source>Volume at %1%</source>
+ <translation>볼륨 %1%</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1425"/>
+ <source>Volume is muted</source>
+ <translation>볼륨 소거됨</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1428"/>
+ <source>Volume is unmuted</source>
+ <translation>볼륨 소거 해제됨</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1435"/>
+ <source>Maximum video definition set to %1</source>
+ <translation>최대 비디오 해상도가 %1(으)로 설정됨</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1476"/>
+ <source>Your privacy is now safe</source>
+ <translation>이제 개인 정보가 안전 합니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1491"/>
+ <source>Downloads complete</source>
+ <translation>다운로드 완료</translation>
+ </message>
+</context>
+<context>
+ <name>MediaView</name>
+ <message>
+ <location filename="src/mediaview.cpp" line="682"/>
+ <source>You can now paste the YouTube link into another application</source>
+ <translation>이제 YouTube 링크를 다른프로그램에 붙여 넣을수 있습니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="690"/>
+ <source>You can now paste the video stream URL into another application</source>
+ <translation>이제 비디오 스트림 URL을 다른프로그램에 붙여 넣을수 있습니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="691"/>
+ <source>The link will be valid only for a limited time.</source>
+ <translation>해당 링크는 제한된 시간 동안만 유효 합니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="780"/>
+ <source>This is just the demo version of %1.</source>
+ <translation>%1의 데모 버전 입니다.%1의 데모 버전 입니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="781"/>
+ <source>It allows you to test the application and see if it works for you.</source>
+ <translation>이 버전으로 프로그램이 사용자 필요에 맞는지 테스트 할수 있습니다.</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="813"/>
+ <source>Continue</source>
+ <translation>계속</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="946"/>
+ <source>of</source>
+ <comment>Used in video parts, as in '2 of 3'</comment>
+ <translation>/</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="959"/>
+ <source>part</source>
+ <comment>This is for video parts, as in 'Cool video - part 1'</comment>
+ <translation>부분</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="961"/>
+ <source>episode</source>
+ <comment>This is for video parts, as in 'Cool series - episode 1'</comment>
+ <translation>에피소드</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="1074"/>
+ <source>Sent from %1</source>
+ <translation>%1에서 전송됨</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="1109"/>
+ <source>Unsubscribe from %1</source>
+ <translation>%1 구독 해제</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="1113"/>
+ <source>Subscribe to %1</source>
+ <translation>%1 구독</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="788"/>
+ <source>Get the full version</source>
+ <translation>풀 버전 구입</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="827"/>
+ <source>Downloading %1</source>
+ <translation>%1 다운로드</translation>
+ </message>
+</context>
+<context>
+ <name>MessageWidget</name>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="21"/>
+ <source>A new version of %1 is available!</source>
+ <translation>%1의 새 버전이 존재 합니다!</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="28"/>
+ <source>%1 %2 is now available. You have %3.</source>
+ <translation>%1 %2이(가) 존재 합니다. 사용자의 사용 버전은 %3 입니다.</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="33"/>
+ <source>Would you like to download it now?</source>
+ <translation>지금 다운로드 할까요?</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="39"/>
+ <source>Skip This Version</source>
+ <translation>이 버전 건너뛰기</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="43"/>
+ <source>Remind Me Later</source>
+ <translation>다음에 알리기</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="47"/>
+ <source>Install Update</source>
+ <translation>업데이트 설치</translation>
+ </message>
+</context>
+<context>
+ <name>PasteLineEdit</name>
+ <message>
+ <location filename="local/src/pastelineedit.cpp" line="6"/>
+ <source>Paste</source>
+ <translation>붙여넣기</translation>
+ </message>
+</context>
+<context>
+ <name>PlaylistItemDelegate</name>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="208"/>
+ <source>%1 views</source>
+ <translation>%1 views</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="332"/>
+ <source>%1 of %2 (%3) — %4</source>
+ <translation>%1 of %2 (%3) — %4</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="339"/>
+ <source>Preparing</source>
+ <translation>준비</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="341"/>
+ <source>Failed</source>
+ <translation>실패</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="343"/>
+ <source>Completed</source>
+ <translation>끝남</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="345"/>
+ <source>Stopped</source>
+ <translation>중지됨</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="381"/>
+ <source>Stop downloading</source>
+ <translation>다운로드 중지</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="391"/>
+ <source>Show in %1</source>
+ <translation>%1에서 보기</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="393"/>
+ <source>Open parent folder</source>
+ <translation>부모 폴더 열기</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="402"/>
+ <source>Restart downloading</source>
+ <translation>다운로드 재시작</translation>
+ </message>
+</context>
+<context>
+ <name>PlaylistModel</name>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="72"/>
+ <source>Searching...</source>
+ <translation>검색중...</translation>
+ </message>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="73"/>
+ <source>Show %1 More</source>
+ <translation>%1 추가 보기</translation>
+ </message>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="74"/>
+ <source>No videos</source>
+ <translation>비디오없음</translation>
+ </message>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="75"/>
+ <source>No more videos</source>
+ <translation>더 이상 비디오 없음</translation>
+ </message>
+</context>
+<context>
+ <name>RefineSearchWidget</name>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="51"/>
+ <source>Sort by</source>
+ <translation>정렬</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="55"/>
+ <source>Relevance</source>
+ <translation>관련성</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="56"/>
+ <location filename="src/refinesearchwidget.cpp" line="72"/>
+ <source>Date</source>
+ <translation>날짜</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="57"/>
+ <source>View Count</source>
+ <translation>재생 횟수</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="58"/>
+ <source>Rating</source>
+ <translation>점수</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="76"/>
+ <source>Anytime</source>
+ <translation>언제나</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="77"/>
+ <source>Today</source>
+ <translation>오늘</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="78"/>
+ <source>7 Days</source>
+ <translation>7일</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="79"/>
+ <source>30 Days</source>
+ <translation>30일</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="93"/>
+ <source>Duration</source>
+ <translation>기간</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="97"/>
+ <location filename="src/refinesearchwidget.cpp" line="124"/>
+ <source>All</source>
+ <translation>모두</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="98"/>
+ <source>Short</source>
+ <translation>짧음</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="99"/>
+ <source>Medium</source>
+ <translation>중간</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="100"/>
+ <source>Long</source>
+ <translation>김</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="103"/>
+ <source>Less than 4 minutes</source>
+ <translation>4분 이하</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="104"/>
+ <source>Between 4 and 20 minutes</source>
+ <translation>4 ~ 20분 사이</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="105"/>
+ <source>Longer than 20 minutes</source>
+ <translation>20분 이상</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="120"/>
+ <source>Quality</source>
+ <translation>품질</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="125"/>
+ <source>High Definition</source>
+ <translation>HD</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="128"/>
+ <source>720p or higher</source>
+ <translation>720p 이상</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="142"/>
+ <source>Done</source>
+ <translation>마침</translation>
+ </message>
+</context>
+<context>
+ <name>RegionsView</name>
+ <message>
+ <location filename="src/regionsview.cpp" line="39"/>
+ <source>Done</source>
+ <translation>마침</translation>
+ </message>
+</context>
+<context>
+ <name>SearchLineEdit</name>
+ <message>
+ <location filename="src/searchlineedit.cpp" line="177"/>
+ <source>Search</source>
+ <translation>검색</translation>
+ </message>
+</context>
+<context>
+ <name>SearchView</name>
+ <message>
+ <location filename="src/searchview.cpp" line="88"/>
+ <source>Welcome to <a href='%1'>%2</a>,</source>
+ <translation><a href='%1'>%2</a>에 오신걸 환영 합니다!</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="195"/>
+ <source>Get the full version</source>
+ <translation>풀 버전 구입</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="114"/>
+ <source>Enter</source>
+ <extracomment>"Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"</extracomment>
+ <translation>언터</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="119"/>
+ <source>a keyword</source>
+ <translation>키워드</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="120"/>
+ <source>a channel</source>
+ <translation>채널</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="125"/>
+ <source>to start watching videos.</source>
+ <translation>비디오 보기 시작</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="149"/>
+ <source>Watch</source>
+ <translation>감상</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="167"/>
+ <source>Recent keywords</source>
+ <translation>최근 키워드</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="180"/>
+ <source>Recent channels</source>
+ <translation>최근 채널</translation>
+ </message>
+</context>
+<context>
+ <name>SidebarHeader</name>
+ <message>
+ <location filename="src/sidebarheader.cpp" line="39"/>
+ <location filename="src/sidebarheader.cpp" line="46"/>
+ <source>&Back</source>
+ <translation>뒤로(&B)</translation>
+ </message>
+ <message>
+ <location filename="src/sidebarheader.cpp" line="77"/>
+ <source>Forward to %1</source>
+ <translation>%1(으)로 이동</translation>
+ </message>
+ <message>
+ <location filename="src/sidebarheader.cpp" line="90"/>
+ <source>Back to %1</source>
+ <translation>%1(으)로 이동</translation>
+ </message>
+</context>
+<context>
+ <name>SidebarWidget</name>
+ <message>
+ <location filename="src/sidebarwidget.cpp" line="68"/>
+ <source>Refine Search</source>
+ <translation>검색 재정의</translation>
+ </message>
+ <message>
+ <location filename="src/sidebarwidget.cpp" line="163"/>
+ <source>Did you mean: %1</source>
+ <translation>원했던 단어: %1</translation>
+ </message>
+</context>
+<context>
+ <name>SnapshotSettings</name>
+ <message>
+ <location filename="src/snapshotsettings.cpp" line="45"/>
+ <source>Change location...</source>
+ <translation>위치 변경...</translation>
+ </message>
+ <message>
+ <location filename="src/snapshotsettings.cpp" line="62"/>
+ <source>Snapshot saved to %1</source>
+ <translation>%1에 스냅샷 저장됨</translation>
+ </message>
+ <message>
+ <location filename="src/snapshotsettings.cpp" line="127"/>
+ <source>Snapshots location changed.</source>
+ <translation>스냅샷 위치 변경됨</translation>
+ </message>
+</context>
+<context>
+ <name>StandardFeedsView</name>
+ <message>
+ <location filename="src/standardfeedsview.cpp" line="105"/>
+ <source>Most Popular</source>
+ <translation>가장 유명</translation>
+ </message>
+</context>
+<context>
+ <name>Video</name>
+ <message>
+ <location filename="src/video.cpp" line="309"/>
+ <source>Cannot get video stream for %1</source>
+ <translation>%1의 비디오 가져올수 없음</translation>
+ </message>
+ <message>
+ <location filename="src/video.cpp" line="326"/>
+ <source>Network error: %1 for %2</source>
+ <translation>네트워크에러: %1 for %2</translation>
+ </message>
+</context>
+<context>
+ <name>YTRegions</name>
+ <message>
+ <location filename="src/ytregions.cpp" line="28"/>
+ <source>Algeria</source>
+ <translation>알제리아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="29"/>
+ <source>Argentina</source>
+ <translation>아르헨티나</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="30"/>
+ <source>Australia</source>
+ <translation>오스트레일리아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="31"/>
+ <source>Belgium</source>
+ <translation>벨기에</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="32"/>
+ <source>Brazil</source>
+ <translation>브라질</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="33"/>
+ <source>Canada</source>
+ <translation>캐나다</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="34"/>
+ <source>Chile</source>
+ <translation>칠레</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="35"/>
+ <source>Colombia</source>
+ <translation>콜롬비아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="36"/>
+ <source>Czech Republic</source>
+ <translation>체코</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="37"/>
+ <source>Egypt</source>
+ <translation>이집트</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="38"/>
+ <source>France</source>
+ <translation>프랑스</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="39"/>
+ <source>Germany</source>
+ <translation>독일</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="40"/>
+ <source>Ghana</source>
+ <translation>가나</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="41"/>
+ <source>Greece</source>
+ <translation>그리스</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="42"/>
+ <source>Hong Kong</source>
+ <translation>홍콩</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="43"/>
+ <source>Hungary</source>
+ <translation>헝가리</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="44"/>
+ <source>India</source>
+ <translation>인디아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="45"/>
+ <source>Indonesia</source>
+ <translation>인도네시아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="46"/>
+ <source>Ireland</source>
+ <translation>아일랜드</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="47"/>
+ <source>Israel</source>
+ <translation>이스라엘</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="48"/>
+ <source>Italy</source>
+ <translation>이탈리아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="49"/>
+ <source>Japan</source>
+ <translation>일본</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="50"/>
+ <source>Jordan</source>
+ <translation>조단</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="51"/>
+ <source>Kenya</source>
+ <translation>케냐</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="52"/>
+ <source>Malaysia</source>
+ <translation>말레이시아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="53"/>
+ <source>Mexico</source>
+ <translation>멕시코</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="54"/>
+ <source>Morocco</source>
+ <translation>모로코</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="55"/>
+ <source>Netherlands</source>
+ <translation>네델란드</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="56"/>
+ <source>New Zealand</source>
+ <translation>뉴질랜드</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="57"/>
+ <source>Nigeria</source>
+ <translation>나이제리아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="58"/>
+ <source>Peru</source>
+ <translation>페루</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="59"/>
+ <source>Philippines</source>
+ <translation>필리핀</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="60"/>
+ <source>Poland</source>
+ <translation>폴란드</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="61"/>
+ <source>Russia</source>
+ <translation>러시아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="62"/>
+ <source>Saudi Arabia</source>
+ <translation>사우디 아라비아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="63"/>
+ <source>Singapore</source>
+ <translation>싱가포르</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="64"/>
+ <source>South Africa</source>
+ <translation>사우스 아프리카</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="65"/>
+ <source>South Korea</source>
+ <translation>대한민국</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="66"/>
+ <source>Spain</source>
+ <translation>스페인</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="67"/>
+ <source>Sweden</source>
+ <translation>스웨덴</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="68"/>
+ <source>Taiwan</source>
+ <translation>대만</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="69"/>
+ <source>Tunisia</source>
+ <translation>투니시아</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="70"/>
+ <source>Turkey</source>
+ <translation>터키</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="71"/>
+ <source>Uganda</source>
+ <translation>우간다</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="72"/>
+ <source>United Arab Emirates</source>
+ <translation>아랍에메레이트</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="73"/>
+ <source>United Kingdom</source>
+ <translation>영국</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="74"/>
+ <source>Yemen</source>
+ <translation>예멘</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="148"/>
+ <source>Worldwide</source>
+ <translation>전세계적</translation>
+ </message>
+</context>
+</TS>
\ No newline at end of file
<message>
<location filename="src/aboutview.cpp" line="52"/>
<source>There's life outside the browser!</source>
- <translation>Браузердин тышындагы жашоо!</translation>
+ <translation>Браузердин тышында да өмүр бар!</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="53"/>
<source>Version %1</source>
- <translation>Ð\96оÑ\80омол %1</translation>
+ <translation>Ð\9dÑ\83Ñ\81ка %1</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="58"/>
<source>Licensed to: %1</source>
- <translation>%1 үчүн лицензияланган</translation>
+ <translation>Лицензиянын ээси: %1</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="62"/>
<message>
<location filename="src/aboutview.h" line="40"/>
<source>About</source>
- <translation>Программа жөнүндө</translation>
+ <translation>Программа тууралуу</translation>
</message>
<message>
<location filename="src/aboutview.h" line="42"/>
<message>
<location filename="local/src/activationview.cpp" line="47"/>
<source>Please license %1</source>
- <translation>Лицензия %1</translation>
+ <translation>%1 лицензиялап алыңыз</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="51"/>
<message>
<location filename="src/mainwindow.cpp" line="501"/>
<source>Share the current video using %1</source>
- <translation>Ð\9aезекÑ\82еги видеону %1 аркылуу бөлүшүү</translation>
+ <translation>УÑ\87Ñ\83Ñ\80дагÑ\8b видеону %1 аркылуу бөлүшүү</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="521"/>
<message>
<location filename="src/mainwindow.cpp" line="1053"/>
<source>Error: %1</source>
- <translation>Катасы: %1</translation>
+ <translation>Ката: %1</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="276"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="76"/>
<source>Anytime</source>
- <translation>Ð\9aаалаган Ñ\83бакÑ\8bÑ\82</translation>
+ <translation>Ð\9aаалаган Ñ\83бакÑ\82а</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="77"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="93"/>
<source>Duration</source>
- <translation>Узундук</translation>
+ <translation>Узактык</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="97"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="100"/>
<source>Long</source>
- <translation>Узун</translation>
+ <translation>Узак</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="103"/>
<message>
<location filename="src/refinesearchwidget.cpp" line="128"/>
<source>720p or higher</source>
- <translation>720p же жогорураак</translation>
+ <translation>720p же жогору</translation>
</message>
<message>
<location filename="src/refinesearchwidget.cpp" line="142"/>
<message>
<location filename="src/ytregions.cpp" line="148"/>
<source>Worldwide</source>
- <translation>Бүткүл дүйнөлүк</translation>
+ <translation>Бүткүл дүйнө</translation>
</message>
</context>
</TS>
\ No newline at end of file
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>Uwie&lbiasz %1? Oceń to!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>A versão compreta permite-lhe ver vídeos sem interrupções.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Marcar como visto</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Cancelar subscrição</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>Take &Snapshot</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>&Gosta? %1? Avalie!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Snapshot guardado em %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>Localização dos snapshots alterada.</translation>
</message>
</context>
<context>
<message>
<location filename="src/aboutview.cpp" line="58"/>
<source>Licensed to: %1</source>
- <translation>Licenciado a: %1</translation>
+ <translation>Licenciado para: %1</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="62"/>
<source>%1 is Free Software but its development takes precious time.</source>
- <translation>%1 é um Software livre, mas seu desenvolvimento toma um tempo precioso.</translation>
+ <translation>%1 é um software livre, mas seu desenvolvimento toma um tempo precioso.</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="63"/>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>A versão completa permite que você assista vídeos sem interrupções.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Marcar como assistido</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Cancelar assinatura</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>&Salvar captura de tela</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<message>
<location filename="src/mainwindow.cpp" line="576"/>
<source>Open in &Browser...</source>
- <translation type="unfinished"/>
+ <translation>Abrir no &navegador...</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>&Gosta do %1? Avalie!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Captura de tela salva em %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>Local de capturas de tela alterado.</translation>
</message>
</context>
<context>
<message>
<location filename="src/aboutview.cpp" line="52"/>
<source>There's life outside the browser!</source>
- <translation>Det finns ett liv utanför webläsaren!</translation>
+ <translation>Det finns ett liv utanför webbläsaren!</translation>
</message>
<message>
<location filename="src/aboutview.cpp" line="53"/>
<location filename="local/src/activationview.cpp" line="53"/>
<source>The full version allows you to watch videos without interruptions.</source>
<oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
- <translation type="unfinished"/>
+ <translation>Den fulla versionen tillåter dig att kolla på videoklipp utan avbrott.</translation>
</message>
<message>
<location filename="local/src/activationview.cpp" line="55"/>
<message>
<location filename="src/channelview.cpp" line="243"/>
<source>Mark as Watched</source>
- <translation type="unfinished"/>
+ <translation>Markera som Sedd</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="256"/>
<source>Unsubscribe</source>
- <translation type="unfinished"/>
+ <translation>Avprenumerera</translation>
</message>
<message>
<location filename="src/channelview.cpp" line="269"/>
<message>
<location filename="src/mainwindow.cpp" line="486"/>
<source>Take &Snapshot</source>
- <translation type="unfinished"/>
+ <translation>Ta &Skärmbild</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="493"/>
<message>
<location filename="src/mainwindow.cpp" line="583"/>
<source>&Love %1? Rate it!</source>
- <translation type="unfinished"/>
+ <translation>&Älskar du %1? Betygsätt den!</translation>
</message>
<message>
<location filename="src/mainwindow.cpp" line="605"/>
<message>
<location filename="src/snapshotsettings.cpp" line="62"/>
<source>Snapshot saved to %1</source>
- <translation type="unfinished"/>
+ <translation>Skärmbild sparad till %1</translation>
</message>
<message>
<location filename="src/snapshotsettings.cpp" line="127"/>
<source>Snapshots location changed.</source>
- <translation type="unfinished"/>
+ <translation>Plats för sparade skärmbilder ändrades.</translation>
</message>
</context>
<context>
--- /dev/null
+<?xml version="1.0" ?><!DOCTYPE TS><TS language="th" version="2.0">
+<defaultcodec>UTF-8</defaultcodec>
+<context>
+ <name>AboutView</name>
+ <message>
+ <location filename="src/aboutview.cpp" line="52"/>
+ <source>There's life outside the browser!</source>
+ <translation>มีชีวิตนอกเบราว์เซอร์!</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="53"/>
+ <source>Version %1</source>
+ <translation>เวอร์ชั่น %1</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="58"/>
+ <source>Licensed to: %1</source>
+ <translation>ถูกลงทะเบียนสู่: %1</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="62"/>
+ <source>%1 is Free Software but its development takes precious time.</source>
+ <translation>%1 เป็นโปรแกรมฟรีแต่การพัฒนามันจะต้องใช้เวลาเป็นอย่างมาก</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="63"/>
+ <source>Please <a href='%1'>donate</a> to support the continued development of %2.</source>
+ <translation>กรุณา <a href='%1'>บริจาค</a> เพื่อสนับสนุนการพัฒนาโปรแกรม %2 ต่อไป</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="67"/>
+ <source>You may want to try my other apps as well:</source>
+ <translation>คุณอาจอยากลองใช้แอปพลิเคชั่นอื่นของฉันเช่นกัน:</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="70"/>
+ <source>%1, a YouTube music player</source>
+ <translation>%1, เครื่องเล่นดนตรี YouTube</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="74"/>
+ <source>%1, a music player</source>
+ <translation>%1, เครื่องเล่นดนตรี</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="80"/>
+ <source>Translate %1 to your native language using %2</source>
+ <translation>แปล %1 สุ่ภาษาของคุณโดยใช้ %2</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="85"/>
+ <source>Icon designed by %1.</source>
+ <translation>ออกแบบไอคอนโดย %1</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="89"/>
+ <source>Released under the <a href='%1'>GNU General Public License</a></source>
+ <translation>ปล่อยเผยแพร่ภายใต้ <a href='%1'>GNU General Public License</a></translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.cpp" line="104"/>
+ <source>&Close</source>
+ <translation>&ปิด</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.h" line="40"/>
+ <source>About</source>
+ <translation>เกี่ยวกับ</translation>
+ </message>
+ <message>
+ <location filename="src/aboutview.h" line="42"/>
+ <source>What you always wanted to know about %1 and never dared to ask</source>
+ <translation>อะไรที่คุณอยากรู้เกี่ยวกับ %1 และไม่เคยกล้าที่จะถาม</translation>
+ </message>
+</context>
+<context>
+ <name>ActivationDialog</name>
+ <message>
+ <location filename="local/src/activationdialog.cpp" line="17"/>
+ <source>Enter your License Details</source>
+ <translation>กรอกรายละเอียดใบอนุญาตของคุณ</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationdialog.cpp" line="29"/>
+ <source>&Email:</source>
+ <translation>&อีเมล:</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationdialog.cpp" line="35"/>
+ <source>&Code:</source>
+ <translation>&รหัส:</translation>
+ </message>
+</context>
+<context>
+ <name>ActivationView</name>
+ <message>
+ <location filename="local/src/activationview.cpp" line="47"/>
+ <source>Please license %1</source>
+ <translation>โปรดซื้อใบอนุญาต %1</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="51"/>
+ <source>This demo has expired.</source>
+ <translation>ชุดทดลองใช้หมดอายุแล้ว</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="53"/>
+ <source>The full version allows you to watch videos without interruptions.</source>
+ <oldsource>The full version allows you to download videos longer than %1 minutes and to watch videos without interruptions.</oldsource>
+ <translation>เวอร์ชั่นเต็มจะอนุญาตให้คุณรับชมวิดีโอโดยไม่ถูกขัดจังหวะ</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="55"/>
+ <source>Without a license, the application will expire in %1 days.</source>
+ <translation>ปราศจากใบอนุญาต แอปพลิเคชั่นจะหมดอายุใน %1 วัน</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="57"/>
+ <source>By purchasing the full version, you will also support the hard work I put into creating %1.</source>
+ <translation>ด้วยการซื้อเวอร์ชั่นเต็ม คุณยังได้สนับสนุนการทำงานหนักที่ฉันใช้ในการสร้าง %1</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="70"/>
+ <source>Use Demo</source>
+ <translation>ใช้ชุดทดลอง</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="77"/>
+ <source>Enter License</source>
+ <translation>กรอกใบอนุญาต</translation>
+ </message>
+ <message>
+ <location filename="local/src/activationview.cpp" line="85"/>
+ <source>Buy License</source>
+ <translation>ซื้อใบอนุญาต</translation>
+ </message>
+</context>
+<context>
+ <name>ChannelAggregator</name>
+ <message>
+ <location filename="src/channelaggregator.cpp" line="131"/>
+ <source>By %1</source>
+ <translation>โดย %1</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="src/channelaggregator.cpp" line="133"/>
+ <source>You have %n new video(s)</source>
+ <translation type="unfinished"><numerusform></numerusform></translation>
+ </message>
+</context>
+<context>
+ <name>ChannelItemDelegate</name>
+ <message>
+ <location filename="src/channelitemdelegate.cpp" line="66"/>
+ <source>All Videos</source>
+ <translation>วิดีโอทั้งหมด</translation>
+ </message>
+ <message>
+ <location filename="src/channelitemdelegate.cpp" line="83"/>
+ <source>Unwatched Videos</source>
+ <translation>วิดีโอที่ไม่ได้รับชม</translation>
+ </message>
+</context>
+<context>
+ <name>ChannelView</name>
+ <message>
+ <location filename="src/channelview.cpp" line="151"/>
+ <source>Mark all as watched</source>
+ <translation>หมายเหตุทั้งหมดว่ารับชมแล้ว</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="159"/>
+ <source>Show Updated</source>
+ <translation>แสดงอัพเดต</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="103"/>
+ <source>Name</source>
+ <translation>ชื่อ</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="110"/>
+ <source>Last Updated</source>
+ <translation>ถูกอัพเดตล่าสุด</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="117"/>
+ <source>Last Added</source>
+ <translation>ถูกเพิ่มล่าสุด</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="124"/>
+ <source>Last Watched</source>
+ <translation>ถูกรับชมล่าสุด</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="131"/>
+ <source>Most Watched</source>
+ <translation>ถูกรับชมมากที่สุด</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="139"/>
+ <source>Sort by</source>
+ <translation>เรียงตาม</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="221"/>
+ <source>All Videos</source>
+ <translation>วิดีโอทั้งหมด</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="225"/>
+ <source>Unwatched Videos</source>
+ <translation>วิดีโอที่ไม่ได้รับชม</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="243"/>
+ <source>Mark as Watched</source>
+ <translation>หมายเหตุว่ารับชมแล้ว</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="256"/>
+ <source>Unsubscribe</source>
+ <translation>เลิกสมัครติดตาม</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="269"/>
+ <source>There are no updated subscriptions at this time.</source>
+ <translation>ไม่มีการสมัครติดตามที่อัพเดต ณ เวลานี้</translation>
+ </message>
+ <message>
+ <location filename="src/channelview.cpp" line="271"/>
+ <source>You have no subscriptions. Use the star symbol to subscribe to channels.</source>
+ <translation>คุณไม่มีการสมัครติดตาม ใช้สัญลักษณ์ดาวเพื่อสมัครติดตามช่อง</translation>
+ </message>
+</context>
+<context>
+ <name>ClearButton</name>
+ <message>
+ <location filename="src/searchlineedit.cpp" line="56"/>
+ <source>Clear</source>
+ <translation>ล้าง</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadItem</name>
+ <message>
+ <location filename="src/downloaditem.cpp" line="406"/>
+ <source>bytes</source>
+ <translation>bytes</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="409"/>
+ <source>KB</source>
+ <translation>KB</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="412"/>
+ <source>MB</source>
+ <translation>MB</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="421"/>
+ <source>bytes/sec</source>
+ <translation>bytes/sec</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="424"/>
+ <source>KB/sec</source>
+ <translation>KB/sec</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="427"/>
+ <source>MB/sec</source>
+ <translation>MB/sec</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="433"/>
+ <source>seconds</source>
+ <translation>วินาที</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="436"/>
+ <source>minutes</source>
+ <translation>นาที</translation>
+ </message>
+ <message>
+ <location filename="src/downloaditem.cpp" line="439"/>
+ <source>%4 %5 remaining</source>
+ <translation>%4 %5 ที่เหลืออยู่</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadManager</name>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="76"/>
+ <source>This is just the demo version of %1.</source>
+ <translation>นี้เป็นแค่เวอร์ชั่นทดลองใช้ของ %1</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="78"/>
+ <source>It can only download videos shorter than %1 minutes so you can test the download functionality.</source>
+ <translation>สามารถดาวน์โหลดวิดึโอส้ันๆกว่า %1 นาที เพื่อทดสอบฟังชั่นดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="84"/>
+ <source>Continue</source>
+ <translation>ต่อไป</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="85"/>
+ <source>Get the full version</source>
+ <translation>รับเวอร์ชั่นเต็ม</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="156"/>
+ <source>%1 downloaded in %2</source>
+ <translation>%1 ถูกดาวน์โหลดใน %2</translation>
+ </message>
+ <message>
+ <location filename="src/downloadmanager.cpp" line="159"/>
+ <source>Download finished</source>
+ <translation>ดาวน์โหลดเสร็จสิ้น</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="src/downloadmanager.cpp" line="164"/>
+ <source>%n Download(s)</source>
+ <translation type="unfinished"><numerusform></numerusform></translation>
+ </message>
+</context>
+<context>
+ <name>DownloadSettings</name>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="35"/>
+ <source>Change location...</source>
+ <translation>เปลี่ยนที่ตั้ง...</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="78"/>
+ <source>Choose the download location</source>
+ <translation>เลือกที่ตั้งการดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="90"/>
+ <source>Download location changed.</source>
+ <translation>ที่ตั้งการดาวน์โหลดถูกเปลี่ยน</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="92"/>
+ <source>Current downloads will still go in the previous location.</source>
+ <translation>การดาวน์โหลดปัจจุบันจะยังคงไปสู่ที่ตั้งก่อนหน้านี้</translation>
+ </message>
+ <message>
+ <location filename="src/downloadsettings.cpp" line="107"/>
+ <source>Downloading to: %1</source>
+ <translation>กำลังดาวน์โหลดสู่: %1</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadView</name>
+ <message>
+ <location filename="src/downloadview.cpp" line="38"/>
+ <location filename="src/downloadview.h" line="45"/>
+ <source>Downloads</source>
+ <translation>ดาวน์โหลด</translation>
+ </message>
+</context>
+<context>
+ <name>DownloadWidget</name>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="60"/>
+ <source>Downloading update...</source>
+ <translation>กำลังดาวน์โหลดอัพเดต...</translation>
+ </message>
+</context>
+<context>
+ <name>GlobalShortcuts</name>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="36"/>
+ <source>Play</source>
+ <translation>เล่น</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="37"/>
+ <source>Pause</source>
+ <translation>พัก</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="38"/>
+ <source>Play/Pause</source>
+ <translation>เล่น/พัก</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="39"/>
+ <source>Stop</source>
+ <translation>หยุด</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="40"/>
+ <source>Stop playing after current track</source>
+ <translation>หยุดเล่นหลังจากแทร็คปัจจุบัน</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="41"/>
+ <source>Next track</source>
+ <translation>แทร็คต่อไป</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="42"/>
+ <source>Previous track</source>
+ <translation>แทร็คก่อนหน้า</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="43"/>
+ <source>Increase volume</source>
+ <translation>เพิ่มความดัง</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="44"/>
+ <source>Decrease volume</source>
+ <translation>ลดความดัง</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="45"/>
+ <source>Mute</source>
+ <translation>เงียบ</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="46"/>
+ <source>Seek forward</source>
+ <translation>หาเดินหน้า</translation>
+ </message>
+ <message>
+ <location filename="src/globalshortcuts.cpp" line="47"/>
+ <source>Seek backward</source>
+ <translation>หาย้อนกลับ</translation>
+ </message>
+</context>
+<context>
+ <name>HomeView</name>
+ <message>
+ <location filename="src/homeview.cpp" line="58"/>
+ <source>Search</source>
+ <translation>ค้นหา</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="60"/>
+ <source>Find videos and channels by keyword</source>
+ <translation>ค้นหาวิดีโอและช่องโดยใช้คำสำคัญ</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="65"/>
+ <source>Browse</source>
+ <translation>เรียกดู</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="67"/>
+ <source>Browse videos by category</source>
+ <translation>เรียกดูวิดีโอตามหมวดหมู่</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="71"/>
+ <source>Subscriptions</source>
+ <translation>การสมัครติดตาม</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.cpp" line="73"/>
+ <source>Channel subscriptions</source>
+ <translation>การสมัครติดตามช่อง</translation>
+ </message>
+ <message>
+ <location filename="src/homeview.h" line="45"/>
+ <source>Make yourself comfortable</source>
+ <translation>ทำตัวเองให้สบาย</translation>
+ </message>
+</context>
+<context>
+ <name>LoadingWidget</name>
+ <message>
+ <location filename="src/loadingwidget.cpp" line="114"/>
+ <source>Error</source>
+ <translation>ผิดพลาด</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="src/mainwindow.cpp" line="267"/>
+ <source>&Stop</source>
+ <translation>&หยุด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="268"/>
+ <source>Stop playback and go back to the search view</source>
+ <translation>หยุดเล่นและกลับสู่มุมมองการค้นหา</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="283"/>
+ <source>S&kip</source>
+ <translation>&ข้าม</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="284"/>
+ <source>Skip to the next video</source>
+ <translation>ข้ามสู่วิดีโอถัดไปไป</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="290"/>
+ <location filename="src/mainwindow.cpp" line="1060"/>
+ <source>&Pause</source>
+ <translation>&พัก</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="291"/>
+ <location filename="src/mainwindow.cpp" line="1061"/>
+ <source>Pause playback</source>
+ <translation>พักการเล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="297"/>
+ <source>&Full Screen</source>
+ <translation>เ&ต็มจอ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="298"/>
+ <source>Go full screen</source>
+ <translation>ทำให้เต็มจอ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="312"/>
+ <source>Hide the playlist and the toolbar</source>
+ <translation>ซ่อนบัญชีการเล่นและแถบเครื่องมือ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="325"/>
+ <source>Go to the YouTube video page and pause playback</source>
+ <translation>ไปที่หน้าวิดีโอ YouTube และพักการเล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="332"/>
+ <source>Copy the current video YouTube link to the clipboard</source>
+ <translation>คัดลอกลิงค์วิดีโอ YouTube ในปัจจุบันไปยังคลิปบอร์ด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="339"/>
+ <source>Copy the current video stream URL to the clipboard</source>
+ <translation>คัดลอก URL กระแสวิดีโอปัจจุบันสู่คลิปบอร์ด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="346"/>
+ <source>Find other video parts hopefully in the right order</source>
+ <translation>ค้นหาตอนอื่นๆของวิดีโอ โดยหวังตามลำดับที่ถูกต้อง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="352"/>
+ <source>&Remove</source>
+ <translation>&ลบออก</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="353"/>
+ <source>Remove the selected videos from the playlist</source>
+ <translation>ลบวิดีโอที่เลือกออกจากบัญชีการเล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="359"/>
+ <source>Move &Up</source>
+ <translation>ย้าย&ขึ้น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="360"/>
+ <source>Move up the selected videos in the playlist</source>
+ <translation>เลื่อนวิดีโอที่เลือกขึ้นบน ในบัญชีการเล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="366"/>
+ <source>Move &Down</source>
+ <translation>ย้าย&ลง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="367"/>
+ <source>Move down the selected videos in the playlist</source>
+ <translation>เลื่อนวิดีโอที่เลือกลงล่าง ในบัญชีการเล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="378"/>
+ <source>Clear the search history. Cannot be undone.</source>
+ <translation>การล้างประวัติการค้นหา ไม่สามารถเลิกทำได้</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="383"/>
+ <source>&Quit</source>
+ <translation>&ออก</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="386"/>
+ <source>Bye</source>
+ <translation>ลาก่อน</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="390"/>
+ <source>&Website</source>
+ <translation>เ&ว็บไซต์</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="392"/>
+ <source>%1 on the Web</source>
+ <translation>%1 บนเว็บ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="398"/>
+ <source>Please support the continued development of %1</source>
+ <translation>กรุณาสนับสนุนการพัฒนาอย่างต่อเนื่องของ %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="403"/>
+ <source>&About</source>
+ <translation>&เกี่ยวกับ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="405"/>
+ <source>Info about %1</source>
+ <translation>ข้อมูลเกี่ยวกับ %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="413"/>
+ <source>Search</source>
+ <translation>ค้นหา</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="432"/>
+ <source>Mute volume</source>
+ <translation>เงียบเสียง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="466"/>
+ <location filename="src/mainwindow.cpp" line="1490"/>
+ <source>&Downloads</source>
+ <translation>&ดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="467"/>
+ <source>Show details about video downloads</source>
+ <translation>แสดงข้อมูลของวิดีโอที่ดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="475"/>
+ <source>&Download</source>
+ <translation>&ดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="476"/>
+ <source>Download the current video</source>
+ <translation>ดาวน์โหลดวิดีโอปัจจุบัน</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="486"/>
+ <source>Take &Snapshot</source>
+ <translation>&ถ่ายภาพ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="493"/>
+ <source>&Subscribe to Channel</source>
+ <translation>&สมัครติดตามช่อง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="501"/>
+ <source>Share the current video using %1</source>
+ <translation>แชร์วิดีโอปัจจุบันโดยใช้ %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="521"/>
+ <source>&Email</source>
+ <translation>&อีเมล</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="522"/>
+ <source>Email</source>
+ <translation>อีเมล</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="527"/>
+ <source>&Close</source>
+ <translation>&ปิด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="537"/>
+ <source>&Float on Top</source>
+ <translation>&วางอยู่บนสุด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="542"/>
+ <source>&Stop After This Video</source>
+ <translation>&หยุดหลังจากวิดีโอนี้</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="549"/>
+ <source>&Report an Issue...</source>
+ <translation>&รายงานปัญหา...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="553"/>
+ <source>&Refine Search...</source>
+ <translation>&ค้นหาโดยละเอียด...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="565"/>
+ <source>More...</source>
+ <translation>เพิ่มเติม...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="568"/>
+ <source>&Related Videos</source>
+ <translation>&วิดีโอที่เกี่ยวข้อง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="570"/>
+ <source>Watch videos related to the current one</source>
+ <translation>รับชมวิดีโอที่เกี่ยวข้องกับรายการปัจจุบัน</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="576"/>
+ <source>Open in &Browser...</source>
+ <translation>เปิดในเ&บราว์เซอร์...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="583"/>
+ <source>&Love %1? Rate it!</source>
+ <translation>&ชอบ %1 มั้ย? ให้คะแนนมันหน่อย!</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="605"/>
+ <source>&Application</source>
+ <translation>&แอปพลิเคชั่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="589"/>
+ <source>Buy %1...</source>
+ <translation>ซื้อ %1...</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="619"/>
+ <source>&Playback</source>
+ <translation>การเ&ล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="633"/>
+ <source>&Playlist</source>
+ <translation>&บัญชีการเล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="642"/>
+ <source>&Video</source>
+ <translation>&วิดีโอ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="658"/>
+ <source>&View</source>
+ <translation>&มุมมอง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="665"/>
+ <source>&Share</source>
+ <translation>แ&บ่งปัน</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="679"/>
+ <source>&Help</source>
+ <translation>&ช่วยเหลือ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="760"/>
+ <source>Press %1 to raise the volume, %2 to lower it</source>
+ <translation>กด %1 เพื่อเพิ่มเสียง กด %2 เพื่อลดเสียง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="955"/>
+ <location filename="src/mainwindow.cpp" line="961"/>
+ <source>Opening %1</source>
+ <translation>กำลังเปิด %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1006"/>
+ <source>Do you want to exit %1 with a download in progress?</source>
+ <translation>คุณจะออกจาก %1 โดยที่การดาวน์โหลดกำลังดำเนินอยู่หรือ?</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1007"/>
+ <source>If you close %1 now, this download will be cancelled.</source>
+ <translation>ถ้าคุณปิด %1 ตอนนี้ การดาวน์โหลดจะถูกยกเลิก</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1012"/>
+ <source>Close and cancel download</source>
+ <translation>ปิดและยกเลิกการดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1013"/>
+ <source>Wait for download to finish</source>
+ <translation>รอให้ดาวน์โหลดเสร็จ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1174"/>
+ <source>Leave &Full Screen</source>
+ <translation>ออกจากแบบเ&ต็มจอ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1601"/>
+ <source>%1 version %2 is now available.</source>
+ <translation>%1 เวอร์ชั่น %2 ตอนนี้มีพร้อมใช้</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1605"/>
+ <source>Remind me later</source>
+ <translation>แจ้งฉันภายหลัง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1606"/>
+ <source>Update</source>
+ <translation>อัพเดต</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1053"/>
+ <source>Error: %1</source>
+ <translation>ผิดพลาด: %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="276"/>
+ <source>P&revious</source>
+ <translation>&ก่อนหน้า</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="277"/>
+ <source>Go back to the previous track</source>
+ <translation>กลับไปที่แทร็คก่อนหน้า</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="311"/>
+ <source>&Compact Mode</source>
+ <translation>โหมด&กะทัดรัด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="324"/>
+ <source>Open the &YouTube Page</source>
+ <translation>เ&ปิดหน้า Youtube</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="331"/>
+ <source>Copy the YouTube &Link</source>
+ <translation>คัดลอก&ลิงค์ Youtube</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="338"/>
+ <source>Copy the Video Stream &URL</source>
+ <translation>คัดลอก &URL ของกระแสวิดีโอ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="345"/>
+ <source>Find Video &Parts</source>
+ <translation>ค้นหา&ตอนของวิดีโอ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="373"/>
+ <source>&Clear Recent Searches</source>
+ <translation>&ล้างการค้นหาเมื่อเร็วๆนี้</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="397"/>
+ <source>Make a &Donation</source>
+ <translation>ทำการ&บริจาค</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="459"/>
+ <source>&Manually Start Playing</source>
+ <translation>เ&ริ่มการเล่นด้วยตนเอง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="460"/>
+ <source>Manually start playing videos</source>
+ <translation>เริ่มการเล่นวิดีโอด้วยตนเอง</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="797"/>
+ <source>Choose your content location</source>
+ <translation>เลือกที่ตั้งเนื้อหาของคุณ</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1073"/>
+ <source>&Play</source>
+ <translation>เ&ล่น</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1074"/>
+ <source>Resume playback</source>
+ <translation>เล่นต่อจากเดิม</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1333"/>
+ <source>Remaining time: %1</source>
+ <translation>เวลาที่เหลือ: %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1419"/>
+ <source>Volume at %1%</source>
+ <translation>ความดังเสียงที่ %1%</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1425"/>
+ <source>Volume is muted</source>
+ <translation>เสียงถูกปิด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1428"/>
+ <source>Volume is unmuted</source>
+ <translation>เสียงถูกเปิด</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1435"/>
+ <source>Maximum video definition set to %1</source>
+ <translation>ใช้วิดีโอความละเอียดสูงสุด %1</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1476"/>
+ <source>Your privacy is now safe</source>
+ <translation>ความเป็นส่วนตัวของคุณขณะนี้ปลอดภัย</translation>
+ </message>
+ <message>
+ <location filename="src/mainwindow.cpp" line="1491"/>
+ <source>Downloads complete</source>
+ <translation>ดาวน์โหลดเสร็จ</translation>
+ </message>
+</context>
+<context>
+ <name>MediaView</name>
+ <message>
+ <location filename="src/mediaview.cpp" line="682"/>
+ <source>You can now paste the YouTube link into another application</source>
+ <translation>ตอนนี้คุณสามารถวางลิงค์ของ YouTube ในแอปพลิเคชั่นอื่นได้</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="690"/>
+ <source>You can now paste the video stream URL into another application</source>
+ <translation>ตอนนี้คุณสามารถวาง URL ของกระแสวิดีโอลงในแอปพลิเคชั่นอื่นได้</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="691"/>
+ <source>The link will be valid only for a limited time.</source>
+ <translation>ลิงค์จะใช้ได้ในระยะเวลาที่จำกัดเท่านั้น</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="780"/>
+ <source>This is just the demo version of %1.</source>
+ <translation>นี้เป็นแค่เวอร์ชั่นทดลองใช้ %1.</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="781"/>
+ <source>It allows you to test the application and see if it works for you.</source>
+ <translation>มันช่วยให้คุณทดสอบโปรแกรมและดูว่ามันใช้ได้ผลกับคุณหรือเปล่า</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="813"/>
+ <source>Continue</source>
+ <translation>ต่อไป</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="946"/>
+ <source>of</source>
+ <comment>Used in video parts, as in '2 of 3'</comment>
+ <translation>จาก</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="959"/>
+ <source>part</source>
+ <comment>This is for video parts, as in 'Cool video - part 1'</comment>
+ <translation>ตอน</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="961"/>
+ <source>episode</source>
+ <comment>This is for video parts, as in 'Cool series - episode 1'</comment>
+ <translation>ภาค</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="1074"/>
+ <source>Sent from %1</source>
+ <translation>ถูกส่งจาก %1</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="1109"/>
+ <source>Unsubscribe from %1</source>
+ <translation>เลิกสมัครติดตามจาก %1</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="1113"/>
+ <source>Subscribe to %1</source>
+ <translation>สมัครติดตามสู่ %1</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="788"/>
+ <source>Get the full version</source>
+ <translation>ทำให้เป็นเวอร์ชั่นเต็ม</translation>
+ </message>
+ <message>
+ <location filename="src/mediaview.cpp" line="827"/>
+ <source>Downloading %1</source>
+ <translation>กำลังดาวน์โหลด %1</translation>
+ </message>
+</context>
+<context>
+ <name>MessageWidget</name>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="21"/>
+ <source>A new version of %1 is available!</source>
+ <translation>เวอร์ชั่นใหม่ของ %1 มีพร้อมแล้ว!</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="28"/>
+ <source>%1 %2 is now available. You have %3.</source>
+ <translation>%1 %2 ตอนนี้มีพร้อมแล้ว คุณมี %3</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="33"/>
+ <source>Would you like to download it now?</source>
+ <translation>คุณอยากจะดาวน์โหลดตอนนี้หรือไม่?</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="39"/>
+ <source>Skip This Version</source>
+ <translation>ข้ามเวอร์ชั่นนี้</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="43"/>
+ <source>Remind Me Later</source>
+ <translation>แจ้งฉันภายหลัง</translation>
+ </message>
+ <message>
+ <location filename="local/src/updatedialog.cpp" line="47"/>
+ <source>Install Update</source>
+ <translation>ติดตั้งอัพเดต</translation>
+ </message>
+</context>
+<context>
+ <name>PasteLineEdit</name>
+ <message>
+ <location filename="local/src/pastelineedit.cpp" line="6"/>
+ <source>Paste</source>
+ <translation>วาง</translation>
+ </message>
+</context>
+<context>
+ <name>PlaylistItemDelegate</name>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="208"/>
+ <source>%1 views</source>
+ <translation>ดู %1 ครั้ง </translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="332"/>
+ <source>%1 of %2 (%3) — %4</source>
+ <translation>%1 ของ %2 (%3) — %4</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="339"/>
+ <source>Preparing</source>
+ <translation>กำลังเตรียม</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="341"/>
+ <source>Failed</source>
+ <translation>ล้มเหลว</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="343"/>
+ <source>Completed</source>
+ <translation>เสร็จสมบูรณ์</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="345"/>
+ <source>Stopped</source>
+ <translation>ถูกหยุด</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="381"/>
+ <source>Stop downloading</source>
+ <translation>หยุดการดาวน์โหลด</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="391"/>
+ <source>Show in %1</source>
+ <translation>แสดงใน %1</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="393"/>
+ <source>Open parent folder</source>
+ <translation>เปิดโฟลเดอร์แม่</translation>
+ </message>
+ <message>
+ <location filename="src/playlistitemdelegate.cpp" line="402"/>
+ <source>Restart downloading</source>
+ <translation>เริ่มดาวน์โหลดใหม่</translation>
+ </message>
+</context>
+<context>
+ <name>PlaylistModel</name>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="72"/>
+ <source>Searching...</source>
+ <translation>กำลังค้นหา...</translation>
+ </message>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="73"/>
+ <source>Show %1 More</source>
+ <translation>แสดง %1 เพิ่มเติม</translation>
+ </message>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="74"/>
+ <source>No videos</source>
+ <translation>ไม่มีวิดีโอ</translation>
+ </message>
+ <message>
+ <location filename="src/playlistmodel.cpp" line="75"/>
+ <source>No more videos</source>
+ <translation>ไม่มีวิดีโอเพิ่มเติม</translation>
+ </message>
+</context>
+<context>
+ <name>RefineSearchWidget</name>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="51"/>
+ <source>Sort by</source>
+ <translation>เรียงตาม</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="55"/>
+ <source>Relevance</source>
+ <translation>ความเกี่ยวข้อง</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="56"/>
+ <location filename="src/refinesearchwidget.cpp" line="72"/>
+ <source>Date</source>
+ <translation>วันที่</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="57"/>
+ <source>View Count</source>
+ <translation>จำนวนการดู</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="58"/>
+ <source>Rating</source>
+ <translation>คะแนน</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="76"/>
+ <source>Anytime</source>
+ <translation>เวลาใดๆ</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="77"/>
+ <source>Today</source>
+ <translation>วันนี้</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="78"/>
+ <source>7 Days</source>
+ <translation>7 วัน</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="79"/>
+ <source>30 Days</source>
+ <translation>30 วัน</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="93"/>
+ <source>Duration</source>
+ <translation>ระยะเวลา</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="97"/>
+ <location filename="src/refinesearchwidget.cpp" line="124"/>
+ <source>All</source>
+ <translation>ทั้งหมด</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="98"/>
+ <source>Short</source>
+ <translation>สั้น</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="99"/>
+ <source>Medium</source>
+ <translation>ปานกลาง</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="100"/>
+ <source>Long</source>
+ <translation>ยาว</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="103"/>
+ <source>Less than 4 minutes</source>
+ <translation>น้อยกว่า 4 นาที</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="104"/>
+ <source>Between 4 and 20 minutes</source>
+ <translation>ระหว่าง 4 ถึง 20 นาที</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="105"/>
+ <source>Longer than 20 minutes</source>
+ <translation>ยาวกว่า 20 นาที</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="120"/>
+ <source>Quality</source>
+ <translation>คุณภาพ</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="125"/>
+ <source>High Definition</source>
+ <translation>ความละเอียดสูง</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="128"/>
+ <source>720p or higher</source>
+ <translation>720p หรือสูงกว่า</translation>
+ </message>
+ <message>
+ <location filename="src/refinesearchwidget.cpp" line="142"/>
+ <source>Done</source>
+ <translation>เสร็จแล้ว</translation>
+ </message>
+</context>
+<context>
+ <name>RegionsView</name>
+ <message>
+ <location filename="src/regionsview.cpp" line="39"/>
+ <source>Done</source>
+ <translation>เสร็จแล้ว</translation>
+ </message>
+</context>
+<context>
+ <name>SearchLineEdit</name>
+ <message>
+ <location filename="src/searchlineedit.cpp" line="177"/>
+ <source>Search</source>
+ <translation>ค้นหา</translation>
+ </message>
+</context>
+<context>
+ <name>SearchView</name>
+ <message>
+ <location filename="src/searchview.cpp" line="88"/>
+ <source>Welcome to <a href='%1'>%2</a>,</source>
+ <translation>ยินดีต้อนรับสู่ <a href='%1'>%2</a>,</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="195"/>
+ <source>Get the full version</source>
+ <translation>ซื้อเวอร์ชั่นเต็ม</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="114"/>
+ <source>Enter</source>
+ <extracomment>"Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"</extracomment>
+ <translation>ตกลง</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="119"/>
+ <source>a keyword</source>
+ <translation>คำสำคัญ</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="120"/>
+ <source>a channel</source>
+ <translation>ช่อง</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="125"/>
+ <source>to start watching videos.</source>
+ <translation>เพื่อเริ่มรับชมวิดีโอ</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="149"/>
+ <source>Watch</source>
+ <translation>รับชม</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="167"/>
+ <source>Recent keywords</source>
+ <translation>คำสำคัญเมื่อเร็วๆนี้</translation>
+ </message>
+ <message>
+ <location filename="src/searchview.cpp" line="180"/>
+ <source>Recent channels</source>
+ <translation>ช่องเมื่อเร็วๆนี้</translation>
+ </message>
+</context>
+<context>
+ <name>SidebarHeader</name>
+ <message>
+ <location filename="src/sidebarheader.cpp" line="39"/>
+ <location filename="src/sidebarheader.cpp" line="46"/>
+ <source>&Back</source>
+ <translation>&กลับ</translation>
+ </message>
+ <message>
+ <location filename="src/sidebarheader.cpp" line="77"/>
+ <source>Forward to %1</source>
+ <translation>เดินหน้าสู่ %1</translation>
+ </message>
+ <message>
+ <location filename="src/sidebarheader.cpp" line="90"/>
+ <source>Back to %1</source>
+ <translation>กลับสู่ %1</translation>
+ </message>
+</context>
+<context>
+ <name>SidebarWidget</name>
+ <message>
+ <location filename="src/sidebarwidget.cpp" line="68"/>
+ <source>Refine Search</source>
+ <translation>ค้นหาโดยละเอียด</translation>
+ </message>
+ <message>
+ <location filename="src/sidebarwidget.cpp" line="163"/>
+ <source>Did you mean: %1</source>
+ <translation>หรือคุณหมายถึง: %1</translation>
+ </message>
+</context>
+<context>
+ <name>SnapshotSettings</name>
+ <message>
+ <location filename="src/snapshotsettings.cpp" line="45"/>
+ <source>Change location...</source>
+ <translation>เปลี่ยนที่บันทึก</translation>
+ </message>
+ <message>
+ <location filename="src/snapshotsettings.cpp" line="62"/>
+ <source>Snapshot saved to %1</source>
+ <translation>ภาพถ่ายถูกบันทึกสู่ %1</translation>
+ </message>
+ <message>
+ <location filename="src/snapshotsettings.cpp" line="127"/>
+ <source>Snapshots location changed.</source>
+ <translation>ที่ตั้งของภาพถ่ายถูกเปลี่ยน</translation>
+ </message>
+</context>
+<context>
+ <name>StandardFeedsView</name>
+ <message>
+ <location filename="src/standardfeedsview.cpp" line="105"/>
+ <source>Most Popular</source>
+ <translation>ยอดนิยม</translation>
+ </message>
+</context>
+<context>
+ <name>Video</name>
+ <message>
+ <location filename="src/video.cpp" line="309"/>
+ <source>Cannot get video stream for %1</source>
+ <translation>ไม่สามารถรับกระแสวิดีโอของ %1</translation>
+ </message>
+ <message>
+ <location filename="src/video.cpp" line="326"/>
+ <source>Network error: %1 for %2</source>
+ <translation>เครือข่ายขัดข้อง: %1 สำหรับ %2</translation>
+ </message>
+</context>
+<context>
+ <name>YTRegions</name>
+ <message>
+ <location filename="src/ytregions.cpp" line="28"/>
+ <source>Algeria</source>
+ <translation>แอลจีเรีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="29"/>
+ <source>Argentina</source>
+ <translation>อาร์เจนติน่า</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="30"/>
+ <source>Australia</source>
+ <translation>ออสเตรเลีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="31"/>
+ <source>Belgium</source>
+ <translation>เบลเยี่ยม</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="32"/>
+ <source>Brazil</source>
+ <translation>บราซิล</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="33"/>
+ <source>Canada</source>
+ <translation>แคนาดา</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="34"/>
+ <source>Chile</source>
+ <translation>ชิลี</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="35"/>
+ <source>Colombia</source>
+ <translation>โคลอมเบีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="36"/>
+ <source>Czech Republic</source>
+ <translation>เชค, สาธารณรัฐ</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="37"/>
+ <source>Egypt</source>
+ <translation>อียิปต์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="38"/>
+ <source>France</source>
+ <translation>ฝรั่งเศส</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="39"/>
+ <source>Germany</source>
+ <translation>เยอรมนี</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="40"/>
+ <source>Ghana</source>
+ <translation>กาน่า</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="41"/>
+ <source>Greece</source>
+ <translation>กรีซ</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="42"/>
+ <source>Hong Kong</source>
+ <translation>ฮ่องกง</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="43"/>
+ <source>Hungary</source>
+ <translation>ฮังการี่</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="44"/>
+ <source>India</source>
+ <translation>อินเดีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="45"/>
+ <source>Indonesia</source>
+ <translation>อินโดนีเซีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="46"/>
+ <source>Ireland</source>
+ <translation>ไอร์แลนด์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="47"/>
+ <source>Israel</source>
+ <translation>อิสราเอล</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="48"/>
+ <source>Italy</source>
+ <translation>อิตาลี่</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="49"/>
+ <source>Japan</source>
+ <translation>ญี่ปุ่น</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="50"/>
+ <source>Jordan</source>
+ <translation>จอร์แดน</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="51"/>
+ <source>Kenya</source>
+ <translation>เคนย่า</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="52"/>
+ <source>Malaysia</source>
+ <translation>มาเลเซีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="53"/>
+ <source>Mexico</source>
+ <translation>เม็กซิโก</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="54"/>
+ <source>Morocco</source>
+ <translation>โมร็อกโก</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="55"/>
+ <source>Netherlands</source>
+ <translation>เนเธอร์แลนด์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="56"/>
+ <source>New Zealand</source>
+ <translation>นิวซีแลนด์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="57"/>
+ <source>Nigeria</source>
+ <translation>ไนจีเรีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="58"/>
+ <source>Peru</source>
+ <translation>เปรู</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="59"/>
+ <source>Philippines</source>
+ <translation>ฟิลิปปินส์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="60"/>
+ <source>Poland</source>
+ <translation>โปแลนด์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="61"/>
+ <source>Russia</source>
+ <translation>รัสเซีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="62"/>
+ <source>Saudi Arabia</source>
+ <translation>ซาอุดิอาระเบีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="63"/>
+ <source>Singapore</source>
+ <translation>สิงคโปร์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="64"/>
+ <source>South Africa</source>
+ <translation>อาฟริกาใต้</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="65"/>
+ <source>South Korea</source>
+ <translation>เกาหลีใต้</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="66"/>
+ <source>Spain</source>
+ <translation>สเปน</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="67"/>
+ <source>Sweden</source>
+ <translation>สวีเดน</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="68"/>
+ <source>Taiwan</source>
+ <translation>ไต้หวัน</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="69"/>
+ <source>Tunisia</source>
+ <translation>ตูนิเซีย</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="70"/>
+ <source>Turkey</source>
+ <translation>ตุรกี</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="71"/>
+ <source>Uganda</source>
+ <translation>ยูกันดา</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="72"/>
+ <source>United Arab Emirates</source>
+ <translation>สหรัฐอาหรับเอมิเรสต์</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="73"/>
+ <source>United Kingdom</source>
+ <translation>สหราชอาณาจักร</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="74"/>
+ <source>Yemen</source>
+ <translation>เยเมน</translation>
+ </message>
+ <message>
+ <location filename="src/ytregions.cpp" line="148"/>
+ <source>Worldwide</source>
+ <translation>ทั่วโลก</translation>
+ </message>
+</context>
+</TS>
\ No newline at end of file
CONFIG += release
TEMPLATE = app
-VERSION = 2.3
+VERSION = 2.4
DEFINES += APP_VERSION="$$VERSION"
APP_NAME = Minitube
DEFINES += APP_PHONON
DEFINES += APP_PHONON_SEEK
DEFINES += APP_SNAPSHOT
+DEFINES += APP_YT3
DEFINES *= QT_NO_DEBUG_OUTPUT
DEFINES *= QT_USE_QSTRINGBUILDER
DEFINES *= QT_STRICT_ITERATORS
+!contains(DEFINES, APP_GOOGLE_API_KEY) {
+ warning("You need to specify a Google API Key, refer to the README.md file for details")
+}
+
TARGET = $${APP_UNIX_NAME}
-QT += network xml sql script
+QT += network sql script
qt:greaterThan(QT_MAJOR_VERSION, 4) {
contains(QT, gui): QT *= widgets
}
src/gridwidget.h \
src/painterutils.h \
src/database.h \
- src/ytuser.h \
src/channelaggregator.h \
src/channelmodel.h \
src/aggregatevideosource.h \
src/seekslider.h \
src/snapshotsettings.h \
src/snapshotpreview.h \
- src/datautils.h
+ src/datautils.h \
+ src/yt3listparser.h \
+ src/ytchannel.h \
+ src/yt3.h \
+ src/paginatedvideosource.h
SOURCES += src/main.cpp \
src/searchlineedit.cpp \
src/urllineedit.cpp \
src/gridwidget.cpp \
src/painterutils.cpp \
src/database.cpp \
- src/ytuser.cpp \
src/channelaggregator.cpp \
src/channelmodel.cpp \
src/aggregatevideosource.cpp \
src/seekslider.cpp \
src/snapshotsettings.cpp \
src/snapshotpreview.cpp \
- src/datautils.cpp
+ src/datautils.cpp \
+ src/yt3listparser.cpp \
+ src/ytchannel.cpp \
+ src/yt3.cpp \
+ src/paginatedvideosource.cpp
RESOURCES += resources.qrc
DESTDIR = build/target/
OBJECTS_DIR = build/obj/
"<p>" + tr("Released under the <a href='%1'>GNU General Public License</a>")
.arg("http://www.gnu.org/licenses/gpl.html") + "</p>"
#endif
- "<p>© 2009-2014 " + Constants::ORG_NAME + "</p>"
+ "<p>© 2009-2015 " + Constants::ORG_NAME + "</p>"
"</body></html>";
QLabel *infoLabel = new QLabel(info, this);
infoLabel->setOpenExternalLinks(true);
AggregateVideoSource::AggregateVideoSource(QObject *parent) :
VideoSource(parent),
- unwatched(false) { }
+ unwatched(false), hasMore(true) { }
-void AggregateVideoSource::loadVideos(int max, int skip) {
+void AggregateVideoSource::loadVideos(int max, int startIndex) {
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
QString sql = "select v.video_id,"
sql += " from subscriptions_videos v order by published desc ";
sql += "limit ?,?";
query.prepare(sql);
- query.bindValue(0, skip - 1);
+ query.bindValue(0, startIndex - 1);
query.bindValue(1, max);
bool success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
video->setId(query.value(0).toString());
video->setPublished(QDateTime::fromTime_t(query.value(1).toUInt()));
video->setTitle(query.value(2).toString());
- video->setAuthor(query.value(3).toString());
- video->setUserId(query.value(4).toString());
+ video->setChannelTitle(query.value(3).toString());
+ video->setChannelId(query.value(4).toString());
video->setDescription(query.value(5).toString());
video->setWebpage(query.value(6).toString());
video->setThumbnailUrl(query.value(7).toString());
video->setDuration(query.value(9).toInt());
videos << video;
}
+
+ hasMore = videos.size() >= max;
+
emit gotVideos(videos);
emit finished(videos.size());
}
+bool AggregateVideoSource::hasMoreVideos() {
+ return hasMore;
+}
+
const QStringList & AggregateVideoSource::getSuggestions() {
QStringList *l = new QStringList();
return *l;
public:
AggregateVideoSource(QObject *parent = 0);
- void loadVideos(int max, int skip);
+ void loadVideos(int max, int startIndex);
+ bool hasMoreVideos();
virtual void abort();
virtual const QStringList & getSuggestions();
QString getName() { return name; }
private:
QString name;
bool unwatched;
+ bool hasMore;
};
$END_LICENSE */
#include "channelaggregator.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#include "ytsearch.h"
#include "searchparams.h"
#include "database.h"
unwatchedCount(-1),
running(false),
stopped(false) {
- QSettings settings;
- checkInterval = settings.value("subscriptionsCheckInterval", 1800).toUInt();
+ checkInterval = 3600;
timer = new QTimer(this);
timer->setInterval(60000 * 5);
}
void ChannelAggregator::start() {
+ stopped = false;
updateUnwatchedCount();
QTimer::singleShot(0, this, SLOT(run()));
- timer->start();
+ if (!timer->isActive()) timer->start();
}
void ChannelAggregator::stop() {
stopped = true;
}
-YTUser* ChannelAggregator::getChannelToCheck() {
+YTChannel* ChannelAggregator::getChannelToCheck() {
if (stopped) return 0;
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
query.prepare("select user_id from subscriptions where checked<? "
"order by checked limit 1");
- query.bindValue(0, QDateTime::currentDateTime().toTime_t() - checkInterval);
+ query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t() - checkInterval);
bool success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
if (query.next())
- return YTUser::forId(query.value(0).toString());
+ return YTChannel::forId(query.value(0).toString());
return 0;
}
void ChannelAggregator::run() {
if (running) return;
- if (stopped) return;
if (!Database::exists()) return;
running = true;
newVideoCount = 0;
updatedChannels.clear();
+
if (!Database::instance().getConnection().transaction())
qWarning() << "Transaction failed" << __PRETTY_FUNCTION__;
+
processNextChannel();
}
void ChannelAggregator::processNextChannel() {
- if (stopped) return;
+ if (stopped) {
+ running = false;
+ return;
+ }
qApp->processEvents();
- YTUser* user = getChannelToCheck();
- if (user) {
+ YTChannel* channel = getChannelToCheck();
+ if (channel) {
SearchParams *params = new SearchParams();
- params->setAuthor(user->getUserId());
+ params->setChannelId(channel->getChannelId());
params->setSortBy(SearchParams::SortByNewest);
params->setTransient(true);
+ params->setPublishedAfter(channel->getChecked());
YTSearch *videoSource = new YTSearch(params, this);
- connect(videoSource, SIGNAL(gotVideos(QList<Video*>)),
- SLOT(videosLoaded(QList<Video*>)));
- videoSource->loadVideos(10, 1);
- user->updateChecked();
+ connect(videoSource, SIGNAL(gotVideos(QList<Video*>)), SLOT(videosLoaded(QList<Video*>)));
+ videoSource->loadVideos(50, 1);
+ channel->updateChecked();
} else finish();
}
void ChannelAggregator::finish() {
- foreach (YTUser *user, updatedChannels)
- if (user->updateNotifyCount())
- emit channelChanged(user);
-
+ /*
+ foreach (YTChannel *channel, updatedChannels)
+ if (channel->updateNotifyCount())
+ emit channelChanged(channel);
updateUnwatchedCount();
+ */
QSqlDatabase db = Database::instance().getConnection();
if (!db.commit())
qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
+
/*
QByteArray b = db.databaseName().right(20).toLocal8Bit();
const char* s = b.constData();
QString channelNames;
const int total = updatedChannels.size();
for (int i = 0; i < total; ++i) {
- YTUser *user = updatedChannels.at(i);
- channelNames += user->getDisplayName();
+ YTChannel *channel = updatedChannels.at(i);
+ channelNames += channel->getDisplayName();
if (i < total-1) channelNames.append(", ");
}
channelNames = tr("By %1").arg(channelNames);
running = false;
}
-void ChannelAggregator::videosLoaded(QList<Video *> videos) {
+void ChannelAggregator::videosLoaded(const QList<Video*> &videos) {
sender()->deleteLater();
+
foreach (Video* video, videos) {
- qApp->processEvents();
addVideo(video);
- video->deleteLater();
+ qApp->processEvents();
}
- processNextChannel();
+
+ if (!videos.isEmpty()) {
+ YTChannel *channel = YTChannel::forId(videos.first()->channelId());
+ channel->updateNotifyCount();
+ emit channelChanged(channel);
+ updateUnwatchedCount();
+ foreach (Video* video, videos) video->deleteLater();
+ }
+
+ QTimer::singleShot(1000, this, SLOT(processNextChannel()));
}
void ChannelAggregator::updateUnwatchedCount() {
// qDebug() << "Inserting" << video->author() << video->title();
- QString userId = video->userId();
- YTUser *user = YTUser::forId(userId);
- if (!updatedChannels.contains(user))
- updatedChannels << user;
- int channelId = user->getId();
+ YTChannel *channel = YTChannel::forId(video->channelId());
+ if (!channel) {
+ qWarning() << "channelId not present in db" << video->channelId() << video->channelTitle();
+ return;
+ }
+
+ if (!updatedChannels.contains(channel))
+ updatedChannels << channel;
- uint now = QDateTime::currentDateTime().toTime_t();
+ uint now = QDateTime::currentDateTimeUtc().toTime_t();
uint published = video->published().toTime_t();
if (published > now) {
qDebug() << "fixing publish time";
"title,author,user_id,description,url,thumb_url,views,duration) "
"values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
query.bindValue(0, video->id());
- query.bindValue(1, channelId);
+ query.bindValue(1, channel->getId());
query.bindValue(2, published);
query.bindValue(3, now);
query.bindValue(4, 0);
query.bindValue(5, video->title());
- query.bindValue(6, video->author());
- query.bindValue(7, video->userId());
+ query.bindValue(6, video->channelTitle());
+ query.bindValue(7, video->channelId());
query.bindValue(8, video->description());
query.bindValue(9, video->webpage());
query.bindValue(10, video->thumbnailUrl());
query = QSqlQuery(db);
query.prepare("update subscriptions set updated=? where user_id=?");
query.bindValue(0, published);
- query.bindValue(1, userId);
+ query.bindValue(1, channel->getChannelId());
success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
}
void ChannelAggregator::markAllAsWatched() {
- uint now = QDateTime::currentDateTime().toTime_t();
+ uint now = QDateTime::currentDateTimeUtc().toTime_t();
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
unwatchedCount = 0;
- foreach (YTUser *user, YTUser::getCachedUsers()) {
- user->setWatched(now);
- user->setNotifyCount(0);
+ foreach (YTChannel *channel, YTChannel::getCachedChannels()) {
+ channel->setWatched(now);
+ channel->setNotifyCount(0);
}
emit unwatchedCountChanged(0);
QSqlDatabase db = Database::instance().getConnection();
QSqlQuery query(db);
query.prepare("update subscriptions_videos set watched=? where video_id=?");
- query.bindValue(0, QDateTime::currentDateTime().toTime_t());
+ query.bindValue(0, QDateTime::currentDateTimeUtc().toTime_t());
query.bindValue(1, video->id());
bool success = query.exec();
if (!success) qWarning() << query.lastQuery() << query.lastError().text();
if (query.numRowsAffected() > 0) {
- YTUser *user = YTUser::forId(video->userId());
- user->updateNotifyCount();
+ YTChannel *channel = YTChannel::forId(video->channelId());
+ channel->updateNotifyCount();
}
}
void ChannelAggregator::cleanup() {
- static const int maxVideos = 1000;
- static const int maxDeletions = 1000;
+ const int maxVideos = 1000;
+ const int maxDeletions = 1000;
if (!Database::exists()) return;
QSqlDatabase db = Database::instance().getConnection();
#include <QtCore>
-class YTUser;
+class YTChannel;
class Video;
class ChannelAggregator : public QObject {
void updateUnwatchedCount();
signals:
- void channelChanged(YTUser*);
+ void channelChanged(YTChannel*);
void unwatchedCountChanged(int count);
private slots:
- void videosLoaded(QList<Video*> videos);
+ void videosLoaded(const QList<Video*> &videos);
+ void processNextChannel();
private:
ChannelAggregator(QObject *parent = 0);
- YTUser* getChannelToCheck();
- void processNextChannel();
+ YTChannel* getChannelToCheck();
void addVideo(Video* video);
void finish();
bool running;
int newVideoCount;
- QList<YTUser*> updatedChannels;
+ QList<YTChannel*> updatedChannels;
QTimer *timer;
bool stopped;
#include "channelitemdelegate.h"
#include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#include "fontutils.h"
#include "channelaggregator.h"
#include "painterutils.h"
void ChannelItemDelegate::paintChannel(QPainter* painter,
const QStyleOptionViewItem& option,
const QModelIndex& index) const {
- const QVariant dataObject = index.data(ChannelModel::DataObjectRole);
- const YTUserPointer channelPointer = dataObject.value<YTUserPointer>();
- YTUser *user = channelPointer.data();
- if (!user) return;
+ YTChannel *channel = index.data(ChannelModel::DataObjectRole).value<YTChannelPointer>().data();
+ if (!channel) return;
painter->save();
// const bool isHovered = index.data(ChannelsModel::HoveredItemRole ).toBool();
// const bool isSelected = option.state & QStyle::State_Selected;
- QPixmap thumbnail = user->getThumbnail();
+ QPixmap thumbnail = channel->getThumbnail();
if (thumbnail.isNull()) {
- user->loadThumbnail();
+ channel->loadThumbnail();
painter->restore();
return;
}
- QString name = user->getDisplayName();
+ QString name = channel->getDisplayName();
drawItem(painter, line, thumbnail, name);
- int notifyCount = user->getNotifyCount();
+ int notifyCount = channel->getNotifyCount();
if (notifyCount > 0)
paintBadge(painter, line, QString::number(notifyCount));
$END_LICENSE */
#include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
static const int channelOffset = 2;
case ChannelModel::DataObjectRole:
if (typeForIndex(index) == ChannelModel::ItemChannel)
- return QVariant::fromValue(QPointer<YTUser>(userForIndex(index)));
+ return QVariant::fromValue(QPointer<YTChannel>(channelForIndex(index)));
break;
case ChannelModel::HoveredItemRole:
case Qt::StatusTipRole:
if (typeForIndex(index) == ChannelModel::ItemChannel)
- return userForIndex(index)->getDescription();
+ return channelForIndex(index)->getDescription();
}
return QVariant();
}
-YTUser* ChannelModel::userForIndex(const QModelIndex &index) const {
+YTChannel* ChannelModel::channelForIndex(const QModelIndex &index) const {
const int row = index.row();
if (row < channelOffset) return 0;
return channels.at(index.row() - channelOffset);
sqlError = q.lastError();
}
while (q.next()) {
- YTUser *user = YTUser::forId(q.value(0).toString());
- connect(user, SIGNAL(thumbnailLoaded()), SLOT(updateSender()), Qt::UniqueConnection);
- connect(user, SIGNAL(notifyCountChanged()), SLOT(updateSender()), Qt::UniqueConnection);
- connect(user, SIGNAL(destroyed(QObject *)), SLOT(removeChannel(QObject *)), Qt::UniqueConnection);
- channels << user;
+ YTChannel *channel = YTChannel::forId(q.value(0).toString());
+ connect(channel, SIGNAL(thumbnailLoaded()), SLOT(updateSender()), Qt::UniqueConnection);
+ connect(channel, SIGNAL(notifyCountChanged()), SLOT(updateSender()), Qt::UniqueConnection);
+ connect(channel, SIGNAL(destroyed(QObject *)), SLOT(removeChannel(QObject *)), Qt::UniqueConnection);
+ channels << channel;
}
endResetModel();
}
}
void ChannelModel::updateSender() {
- YTUser *user = static_cast<YTUser*>(sender());
- if (!user) {
+ YTChannel *channel = static_cast<YTChannel*>(sender());
+ if (!channel) {
qWarning() << "Cannot get sender" << __PRETTY_FUNCTION__;
return;
}
- updateChannel(user);
+ updateChannel(channel);
}
-void ChannelModel::updateChannel(YTUser *user) {
- int row = channels.indexOf(user);
+void ChannelModel::updateChannel(YTChannel *channel) {
+ int row = channels.indexOf(channel);
if (row == -1) return;
row += channelOffset;
QModelIndex i = createIndex(row, 0);
}
void ChannelModel::removeChannel(QObject *obj) {
- YTUser *user = static_cast<YTUser*>(obj);
- qWarning() << "user is" << user << obj << obj->metaObject()->className();
- if (!user) return;
+ YTChannel *channel = static_cast<YTChannel*>(obj);
+ // qWarning() << "channel" << channel << obj << obj->metaObject()->className();
+ if (!channel) return;
- int row = channels.indexOf(user);
+ int row = channels.indexOf(channel);
if (row == -1) return;
int position = row + channelOffset;
#include <QtCore>
#include <QtSql>
-class YTUser;
+class YTChannel;
class ChannelModel : public QAbstractListModel {
void setQuery(const QString &query, const QSqlDatabase &db);
QSqlError lastError() const;
ItemTypes typeForIndex(const QModelIndex &index) const;
- YTUser* userForIndex(const QModelIndex &index) const;
+ YTChannel* channelForIndex(const QModelIndex &index) const;
void setHoveredRow(int row);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
public slots:
void clearHover();
void updateSender();
- void updateChannel(YTUser *user);
+ void updateChannel(YTChannel *channel);
void updateUnwatched();
void removeChannel(QObject *obj);
private:
- QList<YTUser*> channels;
+ QList<YTChannel*> channels;
int hoveredRow;
QSqlError sqlError;
--- /dev/null
+#include "channelsitemdelegate.h"
+#include "channelmodel.h"
+#include "ytuser.h"
+#include "fontutils.h"
+#include "channelaggregator.h"
+#include "painterutils.h"
+
+static const int ITEM_WIDTH = 128;
+static const int ITEM_HEIGHT = 128;
+static const int THUMB_WIDTH = 88;
+static const int THUMB_HEIGHT = 88;
+
+ChannelsItemDelegate::ChannelsItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
+
+}
+
+QSize ChannelsItemDelegate::sizeHint(const QStyleOptionViewItem& /*option*/,
+ const QModelIndex& /*index*/ ) const {
+ return QSize(ITEM_WIDTH, ITEM_HEIGHT);
+}
+
+void ChannelsItemDelegate::paint( QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index ) const {
+ const int itemType = index.data(ChannelModel::ItemTypeRole).toInt();
+ if (itemType == ChannelModel::ItemChannel)
+ paintChannel(painter, option, index);
+ else if (itemType == ChannelModel::ItemAggregate)
+ paintAggregate(painter, option, index);
+ else if (itemType == ChannelModel::ItemUnwatched)
+ paintUnwatched(painter, option, index);
+ else
+ QStyledItemDelegate::paint(painter, option, index);
+}
+
+void ChannelsItemDelegate::paintAggregate(QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) const {
+ painter->save();
+
+ painter->translate(option.rect.topLeft());
+ const QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ static const QPixmap thumbnail = QPixmap(":/images/channels.png");
+
+ QString name = tr("All Videos");
+
+ drawItem(painter, line, thumbnail, name);
+
+ painter->restore();
+}
+
+void ChannelsItemDelegate::paintUnwatched(QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) const {
+ painter->save();
+
+ painter->translate(option.rect.topLeft());
+ const QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ static const QPixmap thumbnail = QPixmap(":/images/unwatched.png");
+
+ QString name = tr("Unwatched Videos");
+
+ drawItem(painter, line, thumbnail, name);
+
+ int notifyCount = ChannelAggregator::instance()->getUnwatchedCount();
+ QString notifyText = QString::number(notifyCount);
+ if (notifyCount > 0) paintBadge(painter, line, notifyText);
+
+ painter->restore();
+}
+
+void ChannelsItemDelegate::paintChannel(QPainter* painter,
+ const QStyleOptionViewItem& option,
+ const QModelIndex& index) const {
+ const QVariant dataObject = index.data(ChannelModel::DataObjectRole);
+ const YTUserPointer channelPointer = dataObject.value<YTUserPointer>();
+ YTUser *user = channelPointer.data();
+ if (!user) return;
+
+ painter->save();
+
+ painter->translate(option.rect.topLeft());
+ const QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ // const bool isActive = index.data( ActiveItemRole ).toBool();
+ // const bool isHovered = index.data(ChannelsModel::HoveredItemRole ).toBool();
+ // const bool isSelected = option.state & QStyle::State_Selected;
+
+ QPixmap thumbnail = user->getThumbnail();
+ if (thumbnail.isNull()) {
+ user->loadThumbnail();
+ painter->restore();
+ return;
+ }
+
+ QString name = user->getDisplayName();
+ drawItem(painter, line, thumbnail, name);
+
+ int notifyCount = user->getNotifyCount();
+ if (notifyCount > 0)
+ paintBadge(painter, line, QString::number(notifyCount));
+
+ painter->restore();
+}
+
+void ChannelsItemDelegate::drawItem(QPainter *painter,
+ const QRect &line,
+ const QPixmap &thumbnail,
+ const QString &name) const {
+ painter->drawPixmap((line.width() - THUMB_WIDTH) / 2, 8, thumbnail);
+
+ QRect nameBox = line;
+ nameBox.adjust(0, 0, 0, -THUMB_HEIGHT - 16);
+ nameBox.translate(0, line.height() - nameBox.height());
+ bool tooBig = false;
+
+#ifdef APP_MAC_NO
+ QFont f = painter->font();
+ f.setFamily("Helvetica");
+ painter->setFont(f);
+#endif
+
+ QRect textBox = painter->boundingRect(nameBox,
+ Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap,
+ name);
+ if (textBox.height() > nameBox.height() || textBox.width() > nameBox.width()) {
+ painter->setFont(FontUtils::small());
+ textBox = painter->boundingRect(nameBox,
+ Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap,
+ name);
+ if (textBox.height() > nameBox.height()) {
+ painter->setClipRect(nameBox);
+ tooBig = true;
+ }
+ }
+ if (tooBig)
+ painter->drawText(nameBox, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, name);
+ else
+ painter->drawText(textBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+}
+
+void ChannelsItemDelegate::paintBadge(QPainter *painter,
+ const QRect &line,
+ const QString &text) const {
+ const int topLeft = (line.width() + THUMB_WIDTH) / 2;
+ painter->save();
+ painter->translate(topLeft, 0);
+ PainterUtils::paintBadge(painter, text, true);
+ painter->restore();
+}
--- /dev/null
+#ifndef CHANNELSITEMDELEGATE_H
+#define CHANNELSITEMDELEGATE_H
+
+#include <QtGui>
+
+class ChannelsItemDelegate : public QStyledItemDelegate {
+
+ Q_OBJECT
+
+public:
+ ChannelsItemDelegate(QObject* parent = 0);
+ QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+
+private:
+ void paintChannel(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paintAggregate(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paintUnwatched(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const;
+ void paintBadge(QPainter *painter, const QRect &line, const QString &text) const;
+ void drawItem(QPainter*, const QRect &line, const QPixmap &thumbnail, const QString &name) const;
+
+};
+
+#endif // CHANNELSITEMDELEGATE_H
--- /dev/null
+#include "channelsmodel.h"
+#include "ytuser.h"
+#include "mainwindow.h"
+
+ChannelsModel::ChannelsModel(QObject *parent) : QSqlQueryModel(parent) {
+ hoveredRow = -1;
+}
+
+QVariant ChannelsModel::data(const QModelIndex &index, int role) const {
+
+ YTUser* user = 0;
+
+ switch (role) {
+
+ case ChannelsModel::ItemTypeRole:
+ return ChannelsModel::ItemChannel;
+ break;
+
+ case ChannelsModel::DataObjectRole:
+ user = userForIndex(index);
+ return QVariant::fromValue(QPointer<YTUser>(user));
+ break;
+
+ case ChannelsModel::HoveredItemRole:
+ return hoveredRow == index.row();
+ break;
+
+ case Qt::StatusTipRole:
+ user = userForIndex(index);
+ return user->getDescription();
+
+ }
+
+ return QVariant();
+}
+
+YTUser* ChannelsModel::userForIndex(const QModelIndex &index) const {
+ return YTUser::forId(QSqlQueryModel::data(QSqlQueryModel::index(index.row(), 0)).toString());
+}
+
+void ChannelsModel::setHoveredRow(int row) {
+ int oldRow = hoveredRow;
+ hoveredRow = row;
+ emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1 ) );
+ emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+}
+
+void ChannelsModel::clearHover() {
+ emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+ hoveredRow = -1;
+}
+
+// --- Sturm und drang ---
+
+
+Qt::DropActions ChannelsModel::supportedDragActions() const {
+ return Qt::CopyAction;
+}
+
+Qt::DropActions ChannelsModel::supportedDropActions() const {
+ return Qt::CopyAction;
+}
+
+Qt::ItemFlags ChannelsModel::flags(const QModelIndex &index) const {
+ if (index.isValid())
+ return Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
+ else return 0;
+}
+
+QStringList ChannelsModel::mimeTypes() const {
+ QStringList types;
+ types << "x-minitube/channel";
+ return types;
+}
+
+QMimeData* ChannelsModel::mimeData( const QModelIndexList &indexes ) const {
+
+ /* TODO
+ UserMimeData* mime = new TrackMimeData();
+
+ foreach( const QModelIndex &index, indexes ) {
+ Item *item = userForIndex(index);
+ if (item) {
+ mime->addTracks(item->getTracks());
+ }
+ }
+
+ return mime;
+ */
+}
--- /dev/null
+#ifndef CHANNELSMODEL_H
+#define CHANNELSMODEL_H
+
+#include <QtSql>
+
+class YTUser;
+
+class ChannelsModel : public QSqlQueryModel {
+
+ Q_OBJECT
+
+public:
+ ChannelsModel(QObject *parent = 0);
+ QVariant data(const QModelIndex &item, int role) const;
+ void setHoveredRow(int row);
+ YTUser* userForIndex(const QModelIndex &index) const;
+
+ enum DataRoles {
+ ItemTypeRole = Qt::UserRole,
+ DataObjectRole,
+ ActiveItemRole,
+ HoveredItemRole
+ };
+
+ enum ItemTypes {
+ ItemChannel = 1,
+ ItemFolder,
+ ItemAggregate
+ };
+
+public slots:
+ void clearHover();
+
+protected:
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ QStringList mimeTypes() const;
+ Qt::DropActions supportedDropActions() const;
+ Qt::DropActions supportedDragActions() const;
+ QMimeData* mimeData( const QModelIndexList &indexes ) const;
+
+private:
+ int hoveredRow;
+};
+
+#endif // CHANNELSMODEL_H
}
void ChannelSuggest::suggest(const QString &query) {
- QUrl url("http://www.youtube.com/results");
+ QUrl url("https://www.youtube.com/results");
#if QT_VERSION >= 0x050000
{
QUrl &u = url;
QList<Suggestion*> suggestions;
QString html = QString::fromUtf8(data);
- QRegExp re("/user/([a-zA-Z0-9]+)");
+ QRegExp re("/(?:user|channel)/[a-zA-Z0-9]+[^>]+data-ytid=[\"']([^\"']+)[\"'][^>]+>([a-zA-Z0-9 ]+)</a>");
int pos = 0;
while ((pos = re.indexIn(html, pos)) != -1) {
- // qDebug() << re.cap(0) << re.cap(1);
- QString choice = re.cap(1);
+ QString choice = re.cap(2);
if (!choices.contains(choice, Qt::CaseInsensitive)) {
- suggestions << new Suggestion(choice);
+ qDebug() << re.capturedTexts();
+ QString channelId = re.cap(1);
+ suggestions << new Suggestion(choice, "channel", channelId);
choices << choice;
if (choices.size() == 10) break;
}
--- /dev/null
+#include "channelsview.h"
+#include "ytuser.h"
+#include "ytsearch.h"
+#include "searchparams.h"
+#include "channelmodel.h"
+#include "channelsitemdelegate.h"
+#include "database.h"
+#include "ytsearch.h"
+#include "channelaggregator.h"
+#include "aggregatevideosource.h"
+#include "painterutils.h"
+#include "mainwindow.h"
+#include "utils.h"
+#ifndef Q_WS_X11
+#include "extra.h"
+#endif
+
+static const char *sortByKey = "subscriptionsSortBy";
+static const char *showUpdatedKey = "subscriptionsShowUpdated";
+
+ChannelsView::ChannelsView(QWidget *parent) : QListView(parent),
+ showUpdated(false),
+ sortBy(SortByName) {
+
+ setItemDelegate(new ChannelsItemDelegate(this));
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+
+ // layout
+ setSpacing(15);
+ setFlow(QListView::LeftToRight);
+ setWrapping(true);
+ setResizeMode(QListView::Adjust);
+ setMovement(QListView::Static);
+ setUniformItemSizes(true);
+
+ // cosmetics
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ setFrameShape(QFrame::NoFrame);
+ setAttribute(Qt::WA_MacShowFocusRect, false);
+
+ QPalette p = palette();
+ p.setColor(QPalette::Disabled, QPalette::Base, p.base().color());
+ p.setColor(QPalette::Disabled, QPalette::Text, p.text().color());
+ setPalette(p);
+
+ verticalScrollBar()->setPageStep(3);
+ verticalScrollBar()->setSingleStep(1);
+
+ setMouseTracking(true);
+
+ connect(this, SIGNAL(clicked(const QModelIndex &)),
+ SLOT(itemActivated(const QModelIndex &)));
+ connect(this, SIGNAL(entered(const QModelIndex &)),
+ SLOT(itemEntered(const QModelIndex &)));
+
+ channelsModel = new ChannelsModel(this);
+ setModel(channelsModel);
+ connect(this, SIGNAL(viewportEntered()),
+ channelsModel, SLOT(clearHover()));
+
+ setupActions();
+
+ connect(ChannelAggregator::instance(), SIGNAL(channelChanged(YTUser*)),
+ channelsModel, SLOT(updateChannel(YTUser*)));
+ connect(ChannelAggregator::instance(), SIGNAL(unwatchedCountChanged(int)),
+ SLOT(unwatchedCountChanged(int)));
+
+ unwatchedCountChanged(ChannelAggregator::instance()->getUnwatchedCount());
+}
+
+void ChannelsView::setupActions() {
+ QSettings settings;
+
+ markAsWatchedAction = new QAction(
+ Utils::icon("mark-watched"), tr("Mark all as watched"), this);
+ markAsWatchedAction->setEnabled(false);
+ markAsWatchedAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_W));
+ connect(markAsWatchedAction, SIGNAL(triggered()), SLOT(markAllAsWatched()));
+ statusActions << markAsWatchedAction;
+
+ showUpdated = settings.value(showUpdatedKey, false).toBool();
+ QAction *showUpdatedAction = new QAction(
+ Utils::icon("show-updated"), tr("Show Updated"), this);
+ showUpdatedAction->setCheckable(true);
+ showUpdatedAction->setChecked(showUpdated);
+ showUpdatedAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_U));
+ connect(showUpdatedAction, SIGNAL(toggled(bool)), SLOT(toggleShowUpdated(bool)));
+ statusActions << showUpdatedAction;
+
+ SortBy sortBy = static_cast<SortBy>(settings.value(sortByKey, SortByName).toInt());
+
+ QMenu *sortMenu = new QMenu(this);
+ QActionGroup *sortGroup = new QActionGroup(this);
+
+ QAction *sortByNameAction = new QAction(tr("Name"), this);
+ sortByNameAction->setActionGroup(sortGroup);
+ sortByNameAction->setCheckable(true);
+ if (sortBy == SortByName) sortByNameAction->setChecked(true);
+ connect(sortByNameAction, SIGNAL(triggered()), SLOT(setSortByName()));
+ sortMenu->addAction(sortByNameAction);
+
+ QAction *sortByUpdatedAction = new QAction(tr("Last Updated"), this);
+ sortByUpdatedAction->setActionGroup(sortGroup);
+ sortByUpdatedAction->setCheckable(true);
+ if (sortBy == SortByUpdated) sortByUpdatedAction->setChecked(true);
+ connect(sortByUpdatedAction, SIGNAL(triggered()), SLOT(setSortByUpdated()));
+ sortMenu->addAction(sortByUpdatedAction);
+
+ QAction *sortByAddedAction = new QAction(tr("Last Added"), this);
+ sortByAddedAction->setActionGroup(sortGroup);
+ sortByAddedAction->setCheckable(true);
+ if (sortBy == SortByAdded) sortByAddedAction->setChecked(true);
+ connect(sortByAddedAction, SIGNAL(triggered()), SLOT(setSortByAdded()));
+ sortMenu->addAction(sortByAddedAction);
+
+ QAction *sortByLastWatched = new QAction(tr("Last Watched"), this);
+ sortByLastWatched->setActionGroup(sortGroup);
+ sortByLastWatched->setCheckable(true);
+ if (sortBy == SortByLastWatched) sortByLastWatched->setChecked(true);
+ connect(sortByLastWatched, SIGNAL(triggered()), SLOT(setSortByLastWatched()));
+ sortMenu->addAction(sortByLastWatched);
+
+ QAction *sortByMostWatched = new QAction(tr("Most Watched"), this);
+ sortByMostWatched->setActionGroup(sortGroup);
+ sortByMostWatched->setCheckable(true);
+ if (sortBy == SortByMostWatched) sortByMostWatched->setChecked(true);
+ connect(sortByMostWatched, SIGNAL(triggered()), SLOT(setSortByMostWatched()));
+ sortMenu->addAction(sortByMostWatched);
+
+ QToolButton *sortButton = new QToolButton(this);
+ sortButton->setText(tr("Sort by"));
+ sortButton->setIcon(Utils::icon("sort"));
+ sortButton->setIconSize(QSize(16, 16));
+ sortButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ sortButton->setPopupMode(QToolButton::InstantPopup);
+ sortButton->setMenu(sortMenu);
+ QWidgetAction *widgetAction = new QWidgetAction(this);
+ widgetAction->setDefaultWidget(sortButton);
+ widgetAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_O));
+ statusActions << widgetAction;
+
+ foreach (QAction *action, statusActions)
+ Utils::setupAction(action);
+}
+
+void ChannelsView::appear() {
+ updateQuery();
+ foreach (QAction* action, statusActions)
+ MainWindow::instance()->showActionInStatusBar(action, true);
+ setFocus();
+ ChannelAggregator::instance()->run();
+}
+
+void ChannelsView::disappear() {
+ foreach (QAction* action, statusActions)
+ MainWindow::instance()->showActionInStatusBar(action, false);
+}
+
+void ChannelsView::mouseMoveEvent(QMouseEvent *event) {
+ QListView::mouseMoveEvent(event);
+ const QModelIndex index = indexAt(event->pos());
+ if (index.isValid()) setCursor(Qt::PointingHandCursor);
+ else unsetCursor();
+}
+
+void ChannelsView::leaveEvent(QEvent *event) {
+ QListView::leaveEvent(event);
+ channelsModel->clearHover();
+}
+
+void ChannelsView::itemEntered(const QModelIndex &index) {
+ // channelsModel->setHoveredRow(index.row());
+}
+
+void ChannelsView::itemActivated(const QModelIndex &index) {
+ ChannelsModel::ItemTypes itemType = channelsModel->typeForIndex(index);
+ if (itemType == ChannelsModel::ItemChannel) {
+ YTUser *user = channelsModel->userForIndex(index);
+ SearchParams *params = new SearchParams();
+ params->setAuthor(user->getUserId());
+ params->setSortBy(SearchParams::SortByNewest);
+ params->setTransient(true);
+ YTSearch *videoSource = new YTSearch(params, this);
+ emit activated(videoSource);
+ user->updateWatched();
+ } else if (itemType == ChannelsModel::ItemAggregate) {
+ AggregateVideoSource *videoSource = new AggregateVideoSource(this);
+ videoSource->setName(tr("All Videos"));
+ emit activated(videoSource);
+ } else if (itemType == ChannelsModel::ItemUnwatched) {
+ AggregateVideoSource *videoSource = new AggregateVideoSource(this);
+ videoSource->setName(tr("Unwatched Videos"));
+ videoSource->setUnwatched(true);
+ emit activated(videoSource);
+ }
+}
+
+void ChannelsView::paintEvent(QPaintEvent *event) {
+ if (model()->rowCount() < 3) {
+ QString msg;
+ if (!errorMessage.isEmpty())
+ msg = errorMessage;
+ else if (showUpdated)
+ msg = tr("There are no updated subscriptions at this time.");
+ else
+ msg = tr("You have no subscriptions. "
+ "Use the star symbol to subscribe to channels.");
+ PainterUtils::centeredMessage(msg, viewport());
+ } else QListView::paintEvent(event);
+ PainterUtils::topShadow(viewport());
+}
+
+void ChannelsView::toggleShowUpdated(bool enable) {
+ showUpdated = enable;
+ updateQuery(true);
+ QSettings settings;
+ settings.setValue(showUpdatedKey, showUpdated);
+}
+
+void ChannelsView::updateQuery(bool transition) {
+ errorMessage.clear();
+ if (!Database::exists()) return;
+
+ QString sql = "select user_id from subscriptions";
+ if (showUpdated)
+ sql += " where notify_count>0";
+
+ switch (sortBy) {
+ case SortByUpdated:
+ sql += " order by updated desc";
+ break;
+ case SortByAdded:
+ sql += " order by added desc";
+ break;
+ case SortByLastWatched:
+ sql += " order by watched desc";
+ break;
+ case SortByMostWatched:
+ sql += " order by views desc";
+ break;
+ default:
+ sql += " order by name collate nocase";
+ break;
+ }
+
+#ifndef Q_WS_X11
+ if (transition)
+ Extra::fadeInWidget(this, this);
+#endif
+
+ channelsModel->setQuery(sql, Database::instance().getConnection());
+ if (channelsModel->lastError().isValid()) {
+ qWarning() << channelsModel->lastError().text();
+ errorMessage = channelsModel->lastError().text();
+ }
+}
+
+void ChannelsView::setSortBy(SortBy sortBy) {
+ this->sortBy = sortBy;
+ updateQuery(true);
+ QSettings settings;
+ settings.setValue(sortByKey, (int)sortBy);
+}
+
+void ChannelsView::markAllAsWatched() {
+ ChannelAggregator::instance()->markAllAsWatched();
+ updateQuery();
+ markAsWatchedAction->setEnabled(false);
+}
+
+void ChannelsView::unwatchedCountChanged(int count) {
+ markAsWatchedAction->setEnabled(count > 0);
+ channelsModel->updateUnwatched();
+ updateQuery();
+}
--- /dev/null
+#ifndef CHANNELSVIEW_H
+#define CHANNELSVIEW_H
+
+#include <QtGui>
+#include "view.h"
+
+class VideoSource;
+class ChannelsModel;
+
+class ChannelsView : public QListView, public View {
+
+ Q_OBJECT
+
+public:
+ ChannelsView(QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *videoSource);
+
+public slots:
+ void appear();
+ void disappear();
+
+protected:
+ void mouseMoveEvent(QMouseEvent *event);
+ void leaveEvent(QEvent *event);
+ void paintEvent(QPaintEvent *event);
+
+private:
+ enum SortBy {
+ SortByName = 0,
+ SortByAdded,
+ SortByUpdated,
+ SortByLastWatched,
+ SortByMostWatched
+ };
+
+private slots:
+ void itemEntered(const QModelIndex &index);
+ void itemActivated(const QModelIndex &index);
+ void toggleShowUpdated(bool enable);
+ void setSortBy(SortBy sortBy);
+ void setSortByName() { setSortBy(SortByName); }
+ void setSortByUpdated() { setSortBy(SortByUpdated); }
+ void setSortByAdded() { setSortBy(SortByAdded); }
+ void setSortByLastWatched() { setSortBy(SortByLastWatched); }
+ void setSortByMostWatched() { setSortBy(SortByMostWatched); }
+ void markAllAsWatched();
+ void unwatchedCountChanged(int count);
+
+private:
+ void updateQuery(bool transition = false);
+ void setupActions();
+
+ ChannelsModel *channelsModel;
+ QList<QAction*> statusActions;
+ bool showUpdated;
+ SortBy sortBy;
+ QString errorMessage;
+ QAction *markAsWatchedAction;
+
+};
+
+#endif // CHANNELSVIEW_H
$END_LICENSE */
#include "channelview.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#include "ytsearch.h"
#include "searchparams.h"
#include "channelmodel.h"
setupActions();
- connect(ChannelAggregator::instance(), SIGNAL(channelChanged(YTUser*)),
- channelsModel, SLOT(updateChannel(YTUser*)));
+ connect(ChannelAggregator::instance(), SIGNAL(channelChanged(YTChannel*)),
+ channelsModel, SLOT(updateChannel(YTChannel*)));
connect(ChannelAggregator::instance(), SIGNAL(unwatchedCountChanged(int)),
SLOT(unwatchedCountChanged(int)));
void ChannelView::setupActions() {
QSettings settings;
- SortBy sortBy = static_cast<SortBy>(settings.value(sortByKey, SortByName).toInt());
+ sortBy = static_cast<SortBy>(settings.value(sortByKey, SortByName).toInt());
QMenu *sortMenu = new QMenu(this);
QActionGroup *sortGroup = new QActionGroup(this);
foreach (QAction* action, statusActions)
MainWindow::instance()->showActionInStatusBar(action, true);
setFocus();
- ChannelAggregator::instance()->run();
+ ChannelAggregator::instance()->start();
}
void ChannelView::disappear() {
+ ChannelAggregator::instance()->stop();
foreach (QAction* action, statusActions)
MainWindow::instance()->showActionInStatusBar(action, false);
}
void ChannelView::itemActivated(const QModelIndex &index) {
ChannelModel::ItemTypes itemType = channelsModel->typeForIndex(index);
if (itemType == ChannelModel::ItemChannel) {
- YTUser *user = channelsModel->userForIndex(index);
+ YTChannel *channel = channelsModel->channelForIndex(index);
SearchParams *params = new SearchParams();
- params->setAuthor(user->getUserId());
+ params->setChannelId(channel->getChannelId());
params->setSortBy(SearchParams::SortByNewest);
params->setTransient(true);
YTSearch *videoSource = new YTSearch(params, this);
+ videoSource->setAsyncDetails(true);
emit activated(videoSource);
- user->updateWatched();
+ channel->updateWatched();
} else if (itemType == ChannelModel::ItemAggregate) {
AggregateVideoSource *videoSource = new AggregateVideoSource(this);
videoSource->setName(tr("All Videos"));
const QModelIndex index = indexAt(point);
if (!index.isValid()) return;
- YTUser *user = channelsModel->userForIndex(index);
- if (!user) return;
+ YTChannel *channel = channelsModel->channelForIndex(index);
+ if (!channel) return;
unsetCursor();
QMenu menu;
- if (user->getNotifyCount() > 0) {
- QAction *markAsWatchedAction = menu.addAction(tr("Mark as Watched"), user, SLOT(updateWatched()));
+ if (channel->getNotifyCount() > 0) {
+ QAction *markAsWatchedAction = menu.addAction(tr("Mark as Watched"), channel, SLOT(updateWatched()));
connect(markAsWatchedAction, SIGNAL(triggered()),
ChannelAggregator::instance(), SLOT(updateUnwatchedCount()));
menu.addSeparator();
notificationsAction->setChecked(true);
*/
- QAction *unsubscribeAction = menu.addAction(tr("Unsubscribe"), user, SLOT(unsubscribe()));
+ QAction *unsubscribeAction = menu.addAction(tr("Unsubscribe"), channel, SLOT(unsubscribe()));
connect(unsubscribeAction, SIGNAL(triggered()),
ChannelAggregator::instance(), SLOT(updateUnwatchedCount()));
--- /dev/null
+#include "channelwidget.h"
+#include "videosource.h"
+#include "ytuser.h"
+#include "fontutils.h"
+
+ChannelWidget::ChannelWidget(VideoSource *videoSource, YTUser *user, QWidget *parent) :
+ GridWidget(parent) {
+ this->user = user;
+ this->videoSource = videoSource;
+
+ setMinimumSize(132, 176);
+ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+
+ connect(user, SIGNAL(infoLoaded()), SLOT(gotUserInfo()));
+ // user->loadfromAPI();
+
+ connect(this, SIGNAL(activated()), SLOT(activate()));
+}
+
+void ChannelWidget::activate() {
+ // YTUser.checked(user->getUserId());
+ emit activated(videoSource);
+}
+
+void ChannelWidget::gotUserInfo() {
+ connect(user, SIGNAL(thumbnailLoaded(QByteArray)), SLOT(gotUserThumbnail(QByteArray)));
+ // user->loadThumbnail();
+ update();
+}
+
+void ChannelWidget::paintEvent(QPaintEvent *) {
+ if (thumbnail.isNull()) return;
+
+ const int w = width();
+ const int h = height();
+
+ QPainter p(this);
+ p.drawPixmap((w - thumbnail.width()) / 2, 0, thumbnail);
+ //(h - thumbnail.height()) / 2
+
+ QRect nameBox = rect();
+ nameBox.adjust(0, 0, 0, -thumbnail.height() - 10);
+ nameBox.translate(0, h - nameBox.height());
+
+ QString name = user->getDisplayName();
+ bool tooBig = false;
+ p.save();
+ /*
+ QFont f = font();
+ f.setFamily("Helvetica");
+ p.setFont(f);
+ */
+ QRect textBox = p.boundingRect(nameBox, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, name);
+ if (textBox.height() > nameBox.height()) {
+ p.setFont(font());
+ textBox = p.boundingRect(nameBox, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, name);
+ if (textBox.height() > nameBox.height()) {
+ p.setClipRect(nameBox);
+ tooBig = true;
+ }
+ }
+ p.setPen(Qt::black);
+ if (tooBig)
+ p.drawText(nameBox, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, name);
+ else
+ p.drawText(textBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+ p.restore();
+
+ if (hasFocus()) {
+ p.save();
+ QPen pen;
+ pen.setBrush(palette().highlight());
+ pen.setWidth(2);
+ p.setPen(pen);
+ p.drawRect(rect());
+ p.restore();
+ }
+}
+
+void ChannelWidget::gotUserThumbnail(QByteArray bytes) {
+ thumbnail.loadFromData(bytes);
+ if (thumbnail.width() > 88)
+ thumbnail = thumbnail.scaledToWidth(88, Qt::SmoothTransformation);
+ update();
+}
--- /dev/null
+#ifndef CHANNELWIDGET_H
+#define CHANNELWIDGET_H
+
+#include <QtGui>
+#include "gridwidget.h"
+
+class VideoSource;
+class YTUser;
+
+class ChannelWidget : public GridWidget {
+
+ Q_OBJECT
+
+public:
+ ChannelWidget(VideoSource *videoSource, YTUser *user, QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *videoSource);
+
+protected:
+ void paintEvent(QPaintEvent *event);
+
+private slots:
+ void gotUserInfo();
+ void gotUserThumbnail(QByteArray bytes);
+ void activate();
+
+private:
+ QPixmap thumbnail;
+ YTUser *user;
+ VideoSource *videoSource;
+
+};
+
+#endif // CHANNELWIDGET_H
QMutexLocker locker(&lock);
- if(QFile::exists(dbLocation)) {
+ if (QFile::exists(dbLocation)) {
// check db version
int databaseVersion = getAttribute("version").toInt();
- if (databaseVersion != DATABASE_VERSION)
+ if (databaseVersion > DATABASE_VERSION)
qWarning("Wrong database version: %d", databaseVersion);
+
+ if (!getAttribute("channelIdFix").toBool())
+ fixChannelIds();
+
} else createDatabase();
}
QSqlQuery("create table subscriptions ("
"id integer primary key autoincrement,"
- "user_id varchar,"
- "user_name varchar,"
- "name varchar,"
+ "user_id varchar," // this is really channel_id
+ "user_name varchar," // obsolete yt2 username
+ "name varchar," // this is really channel_title
"description varchar,"
"thumb_url varchar,"
"country varchar,"
QSqlQuery("create table subscriptions_videos ("
"id integer primary key autoincrement,"
"video_id varchar,"
- "channel_id integer,"
+ "channel_id integer," // this is really subscription_id
"published integer,"
"added integer,"
"watched integer,"
"title varchar,"
- "author varchar,"
- "user_id varchar,"
+ "author varchar," // this is really channel_title
+ "user_id varchar," // this is really channel_id
"description varchar,"
"url varchar,"
"thumb_url varchar,"
void Database::setAttribute(QString name, QVariant value) {
QSqlQuery query(getConnection());
- query.prepare("update attributes set value=? where name=?");
- query.bindValue(0, value);
- query.bindValue(1, name);
+ query.prepare("insert or replace into attributes (name, value) values (?,?)");
+ query.bindValue(0, name);
+ query.bindValue(1, value);
bool success = query.exec();
- if (!success) qDebug() << query.lastError().text();
+ if (!success) qWarning() << query.lastError().text();
+}
+
+void Database::fixChannelIds() {
+ if (!getConnection().transaction())
+ qWarning() << "Transaction failed" << __PRETTY_FUNCTION__;
+
+ qWarning() << "Fixing channel ids";
+
+ QSqlQuery query(getConnection());
+ bool success = query.exec("update subscriptions set user_id='UC' || user_id where user_id not like 'UC%'");
+ if (!success) qWarning() << query.lastError().text();
+
+ query = QSqlQuery(getConnection());
+ success = query.exec("update subscriptions_videos set user_id='UC' || user_id where user_id not like 'UC%'");
+ if (!success) qWarning() << query.lastError().text();
+
+ setAttribute("channelIdFix", 1);
+
+ if (!getConnection().commit())
+ qWarning() << "Commit failed" << __PRETTY_FUNCTION__;
}
/**
QVariant getAttribute(QString name);
void setAttribute(QString name, QVariant value);
+ void fixChannelIds();
+
QMutex lock;
QString dbLocation;
QHash<QThread*, QSqlDatabase> connections;
return f;
}
+
+QString DataUtils::regioneCode(const QLocale &locale) {
+ QString name = locale.name();
+ int index = name.indexOf('_');
+ if (index == -1) return QString();
+ return name.right(index);
+}
+
+QString DataUtils::systemRegioneCode() {
+ return regioneCode(QLocale::system());
+}
+
+uint DataUtils::parseIsoPeriod(const QString &isoPeriod) {
+ // QTime time = QTime::fromString("1mm12car00", "PT8M50S");
+ // ^P((\d+Y)?(\d+M)?(\d+W)?(\d+D)?)?(T(\d+H)?(\d+M)?(\d+S)?)?$
+ /*
+ QRegExp isoPeriodRE("^PT(\d+H)?(\d+M)?(\d+S)?)?$");
+ if (!isoPeriodRE.indexIn(isoPeriod)) {
+ qWarning() << "Cannot parse ISO period" << isoPeriod;
+ continue;
+ }
+
+ int totalCaptures = isoPeriodRE.capturedTexts();
+ for (int i = totalCaptures; i > 0; --i) {
+
+ }
+ */
+
+ uint days = 0, hours = 0, minutes = 0, seconds = 0;
+
+ const char *ptr = isoPeriod.toStdString().c_str();
+ while (*ptr) {
+ if(*ptr == 'P' || *ptr == 'T') {
+ ptr++;
+ continue;
+ }
+
+ int value, charsRead;
+ char type;
+ if (sscanf(ptr, "%d%c%n", &value, &type, &charsRead) != 2)
+ continue;
+
+ if (type == 'D')
+ days = value;
+ else if (type == 'H')
+ hours = value;
+ else if (type == 'M')
+ minutes = value;
+ else if (type == 'S')
+ seconds = value;
+
+ ptr += charsRead;
+ }
+
+ uint period = ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
+ return period;
+}
public:
static QString stringToFilename(const QString &s);
+ static QString regioneCode(const QLocale &locale);
+ static QString systemRegioneCode();
+ static uint parseIsoPeriod(const QString &isoPeriod);
private:
DataUtils() { }
QIODevice* DiskCache::prepare(const QNetworkCacheMetaData &metaData) {
QString mime;
- foreach (QNetworkCacheMetaData::RawHeader header, metaData.rawHeaders()) {
+ foreach (const QNetworkCacheMetaData::RawHeader &header, metaData.rawHeaders()) {
// qDebug() << header.first << header.second;
if (header.first.constData() == QLatin1String("Content-Type")) {
mime = header.second;
}
}
- if (mime.startsWith(QLatin1String("image/")) ||
- mime.endsWith(QLatin1String("/javascript")))
+ if (mime == QLatin1String("application/json") || mime.startsWith(QLatin1String("image/"))) {
return QNetworkDiskCache::prepare(metaData);
+ }
return 0;
}
+
+QNetworkCacheMetaData DiskCache::metaData(const QUrl &url) {
+ // Remove "key" from query string in order to reuse cache when key changes
+ static const QString keyQueryItem = "key";
+ if (url.hasQueryItem(keyQueryItem)) {
+ QUrl url2(url);
+ url2.removeQueryItem(keyQueryItem);
+ return QNetworkDiskCache::metaData(url2);
+ }
+ return QNetworkDiskCache::metaData(url);
+}
public:
explicit DiskCache(QObject *parent = 0);
QIODevice* prepare(const QNetworkCacheMetaData &metaData);
+ QNetworkCacheMetaData metaData(const QUrl &url);
signals:
}
JsFunctions* JsFunctions::instance() {
- static JsFunctions *i = new JsFunctions();
+ static JsFunctions *i = new JsFunctions(QLatin1String(Constants::WEBSITE) + "-ws/functions.js");
return i;
}
-JsFunctions::JsFunctions(QObject *parent) : QObject(parent), engine(0) {
+JsFunctions::JsFunctions(const QString &url, QObject *parent) : QObject(parent), url(url), engine(0) {
QFile file(jsPath());
if (file.exists()) {
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
parseJs(QString::fromUtf8(file.readAll()));
else
- qWarning() << file.errorString() << file.fileName();
+ qWarning() << "Cannot open" << file.errorString() << file.fileName();
QFileInfo info(file);
- if (info.lastModified().toTime_t() < QDateTime::currentDateTime().toTime_t() - 1800)
- loadJs();
+ bool stale = info.size() == 0 || info.lastModified().toTime_t() < QDateTime::currentDateTime().toTime_t() - 1800;
+ if (stale) loadJs();
} else {
QFile resFile(QLatin1String(":/") + jsFilename());
resFile.open(QIODevice::ReadOnly | QIODevice::Text);
void JsFunctions::parseJs(const QString &js) {
if (js.isEmpty()) return;
+ // qDebug() << "Parsing" << js;
if (engine) delete engine;
- engine = new QScriptEngine();
+ engine = new QScriptEngine(this);
engine->evaluate(js);
+ emit ready();
}
-const QLatin1String & JsFunctions::jsFilename() {
- static const QLatin1String filename("functions.js");
- return filename;
+QString JsFunctions::jsFilename() {
+ return QFileInfo(url).fileName();
}
-const QString & JsFunctions::jsPath() {
- static const QString path(
+QString JsFunctions::jsPath() {
+ return QString(
#if QT_VERSION >= 0x050000
QStandardPaths::writableLocation(QStandardPaths::DataLocation)
#else
QDesktopServices::storageLocation(QDesktopServices::DataLocation)
#endif
+ "/" + jsFilename());
- return path;
}
void JsFunctions::loadJs() {
- QUrl url(QLatin1String(Constants::WEBSITE) + "-ws/" + jsFilename());
+ QUrl url(this->url);
url.addQueryItem("v", Constants::VERSION);
NetworkReply* reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(gotJs(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorJs(QNetworkReply*)));
}
-void JsFunctions::gotJs(QByteArray bytes) {
- parseJs(QString::fromUtf8(bytes));
+void JsFunctions::gotJs(const QByteArray &bytes) {
+ if (bytes.isEmpty()) {
+ qWarning() << "Got empty js";
+ return;
+ }
QFile file(jsPath());
- if (!file.open(QIODevice::WriteOnly))
- qWarning() << file.errorString() << file.fileName();
+ if (!file.open(QIODevice::WriteOnly)) {
+ qWarning() << "Cannot write" << file.errorString() << file.fileName();
+ return;
+ }
QDataStream stream(&file);
stream.writeRawData(bytes.constData(), bytes.size());
+ parseJs(QString::fromUtf8(bytes));
}
void JsFunctions::errorJs(QNetworkReply *reply) {
qWarning() << "Cannot get" << jsFilename() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()
- << reply->url().toString() << reply->errorString();
+ << reply->url().toString() << reply->errorString();
}
-QString JsFunctions::evaluate(const QString &js) {
+QScriptValue JsFunctions::evaluate(const QString &js) {
if (!engine) return QString();
QScriptValue value = engine->evaluate(js);
if (value.isUndefined())
if (value.isError())
qWarning() << "Error in" << js << value.toString();
- return value.toString();
+ return value;
+}
+
+QString JsFunctions::string(const QString &js) {
+ return evaluate(js).toString();
+}
+
+QStringList JsFunctions::stringArray(const QString &js) {
+ QStringList items;
+ QScriptValue array = evaluate(js);
+ if (!array.isArray()) return items;
+ QScriptValueIterator it(array);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue value = it.value();
+ if (!value.isString()) continue;
+ items << value.toString();
+ }
+ return items;
}
QString JsFunctions::decryptSignature(const QString &s) {
- return evaluate("decryptSignature('" + s + "')");
+ return string("decryptSignature('" + s + "')");
}
QString JsFunctions::decryptAgeSignature(const QString &s) {
- return evaluate("decryptAgeSignature('" + s + "')");
+ return string("decryptAgeSignature('" + s + "')");
}
QString JsFunctions::videoIdRE() {
- return evaluate("videoIdRE()");
+ return string("videoIdRE()");
}
QString JsFunctions::videoTokenRE() {
- return evaluate("videoTokenRE()");
+ return string("videoTokenRE()");
}
QString JsFunctions::videoInfoFmtMapRE() {
- return evaluate("videoInfoFmtMapRE()");
+ return string("videoInfoFmtMapRE()");
}
QString JsFunctions::webPageFmtMapRE() {
- return evaluate("webPageFmtMapRE()");
+ return string("webPageFmtMapRE()");
}
QString JsFunctions::ageGateRE() {
- return evaluate("ageGateRE()");
+ return string("ageGateRE()");
}
QString JsFunctions::jsPlayerRE() {
- return evaluate("jsPlayerRE()");
+ return string("jsPlayerRE()");
}
QString JsFunctions::signatureFunctionNameRE() {
- return evaluate("signatureFunctionNameRE()");
+ return string("signatureFunctionNameRE()");
+}
+
+QStringList JsFunctions::apiKeys() {
+ return stringArray("apiKeys()");
}
public:
static JsFunctions* instance();
+ JsFunctions(const QString &url, QObject *parent = 0);
+ QScriptValue evaluate(const QString &js);
+ QString string(const QString &js);
+ QStringList stringArray(const QString &js);
// Specialized functions
// TODO move to subclass
QString ageGateRE();
QString jsPlayerRE();
QString signatureFunctionNameRE();
+ QStringList apiKeys();
-protected:
- QString evaluate(const QString &js);
+signals:
+ void ready();
private slots:
- void gotJs(QByteArray bytes);
+ void gotJs(const QByteArray &bytes);
void errorJs(QNetworkReply *reply);
private:
- JsFunctions(QObject *parent = 0);
- static const QLatin1String &jsFilename();
- static const QString &jsPath();
+ QString jsFilename();
+ QString jsPath();
void loadJs();
void parseJs(const QString &js);
+ QString url;
QScriptEngine *engine;
};
#include "mac_startup.h"
#endif
+void showWindow(QtSingleApplication &app, const QString &dataDir) {
+ MainWindow *mainWin = new MainWindow();
+ mainWin->show();
+
+#ifndef APP_MAC
+ QIcon appIcon;
+ if (QDir(dataDir).exists()) {
+ appIcon = IconUtils::icon(Constants::UNIX_NAME);
+ } else {
+ QString dataDir = qApp->applicationDirPath() + "/data";
+ const int iconSizes [] = { 16, 22, 32, 48, 64, 128, 256, 512 };
+ for (int i = 0; i < 8; i++) {
+ QString size = QString::number(iconSizes[i]);
+ QString png = dataDir + "/" + size + "x" + size + "/" + Constants::UNIX_NAME + ".png";
+ appIcon.addFile(png, QSize(iconSizes[i], iconSizes[i]));
+ }
+ }
+ if (appIcon.isNull()) appIcon.addFile(":/images/app.png");
+ mainWin->setWindowIcon(appIcon);
+#endif
+
+ mainWin->connect(&app, SIGNAL(messageReceived(const QString &)),
+ mainWin, SLOT(messageReceived(const QString &)));
+ app.setActivationWindow(mainWin, true);
+}
+
int main(int argc, char **argv) {
#ifdef Q_OS_MAC
mac::MacMain();
- QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Helvetica Neue");
+ // QFont::insertSubstitution(".Helvetica Neue DeskInterface", "Helvetica Neue");
#endif
QtSingleApplication app(argc, argv);
QTextCodec::setCodecForTr(QTextCodec::codecForName("utf8"));
#endif
- MainWindow mainWin;
- mainWin.show();
-
- // no window icon on Mac
-#ifndef APP_MAC
- QIcon appIcon;
- if (QDir(dataDir).exists()) {
- appIcon = IconUtils::icon(Constants::UNIX_NAME);
- } else {
- dataDir = qApp->applicationDirPath() + "/data";
- const int iconSizes [] = { 16, 22, 32, 48, 64, 128, 256, 512 };
- for (int i = 0; i < 8; i++) {
- QString size = QString::number(iconSizes[i]);
- QString png = dataDir + "/" + size + "x" + size + "/" +
- Constants::UNIX_NAME + ".png";
- appIcon.addFile(png, QSize(iconSizes[i], iconSizes[i]));
- }
- }
- if (appIcon.isNull()) {
- appIcon.addFile(":/images/app.png");
- }
- mainWin.setWindowIcon(appIcon);
-#endif
-
- mainWin.connect(&app, SIGNAL(messageReceived(const QString &)),
- &mainWin, SLOT(messageReceived(const QString &)));
- app.setActivationWindow(&mainWin, true);
+ // Seed random number generator
+ qsrand(QDateTime::currentDateTime().toTime_t());
// all string literals are UTF-8
// QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
- // Seed random number generator
- qsrand(QDateTime::currentDateTime().toTime_t());
+#ifdef APP_INTEGRITY
+ if (Extra::integrityCheck())
+#endif
+ showWindow(app, dataDir);
return app.exec();
}
#include "videoareawidget.h"
#include "jsfunctions.h"
#include "seekslider.h"
+#ifdef APP_YT3
+#include "yt3.h"
+#endif
+namespace {
static MainWindow *singleton = 0;
+}
MainWindow* MainWindow::instance() {
- if (!singleton) singleton = new MainWindow();
return singleton;
}
singleton = this;
+#ifdef APP_EXTRA
+ Extra::windowSetup(this);
+#endif
+
// views mechanism
history = new QStack<QWidget*>();
views = new QStackedWidget();
views->show();
-#ifdef APP_EXTRA
- Extra::windowSetup(this);
-#endif
-
qApp->processEvents();
QTimer::singleShot(50, this, SLOT(lazyInit()));
}
JsFunctions::instance();
checkForUpdate();
-
- ChannelAggregator::instance()->start();
}
void MainWindow::changeEvent(QEvent* event) {
// qDebug() << obj << mouseEvent->pos() << isHoveringVideo << mediaView->isPlaylistVisible();
- if (mediaView->isPlaylistVisible()) {
+ if (mediaView && mediaView->isPlaylistVisible()) {
if (isHoveringVideo && x > 5) mediaView->setPlaylistVisible(false);
} else {
if (isHoveringVideo && x >= 0 && x < 5) mediaView->setPlaylistVisible(true);
QAction *definitionAct = new QAction(this);
#ifdef Q_OS_LINUX
definitionAct->setIcon(IconUtils::tintedIcon("video-display", QColor(0, 0, 0),
- QList<QSize>() << QSize(16, 16)));
+ QList<QSize>() << QSize(16, 16)));
#else
definitionAct->setIcon(IconUtils::icon("video-display"));
#endif
mainToolBar->addWidget(new Spacer());
mainToolBar->addAction(volumeMuteAct);
+#ifdef Q_WS_X11
+ QToolButton *volumeMuteButton = qobject_cast<QToolButton *>(mainToolBar->widgetForAction(volumeMuteAct));
+ volumeMuteButton->setIcon(volumeMuteButton->icon().pixmap(16));
+#endif
#ifdef APP_PHONON
volumeSlider = new Phonon::VolumeSlider(this);
if (!m_fullscreen && !compactViewAct->isChecked()) {
writeSettings();
}
- mediaView->stop();
+ // mediaView->stop();
Temporary::deleteAll();
ChannelAggregator::instance()->stop();
ChannelAggregator::instance()->cleanup();
mediaObject = new Phonon::MediaObject(this);
mediaObject->setTickInterval(100);
connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
- this, SLOT(stateChanged(Phonon::State, Phonon::State)));
- connect(mediaObject, SIGNAL(tick(qint64)), this, SLOT(tick(qint64)));
- connect(mediaObject, SIGNAL(totalTimeChanged(qint64)), this, SLOT(totalTimeChanged(qint64)));
+ SLOT(stateChanged(Phonon::State, Phonon::State)));
+ connect(mediaObject, SIGNAL(tick(qint64)), SLOT(tick(qint64)));
+ connect(mediaObject, SIGNAL(totalTimeChanged(qint64)), SLOT(totalTimeChanged(qint64)));
+
+ audioOutput = new Phonon::AudioOutput(Phonon::VideoCategory, this);
+ connect(audioOutput, SIGNAL(volumeChanged(qreal)), SLOT(volumeChanged(qreal)));
+ connect(audioOutput, SIGNAL(mutedChanged(bool)), SLOT(volumeMutedChanged(bool)));
+ Phonon::createPath(mediaObject, audioOutput);
+ volumeSlider->setAudioOutput(audioOutput);
+
#ifdef APP_PHONON_SEEK
seekSlider->setMediaObject(mediaObject);
#endif
- audioOutput = new Phonon::AudioOutput(Phonon::VideoCategory, this);
- connect(audioOutput, SIGNAL(volumeChanged(qreal)), this, SLOT(volumeChanged(qreal)));
- connect(audioOutput, SIGNAL(mutedChanged(bool)), this, SLOT(volumeMutedChanged(bool)));
- volumeSlider->setAudioOutput(audioOutput);
- Phonon::createPath(mediaObject, audioOutput);
+
QSettings settings;
- audioOutput->setVolume(settings.value("volume", 1).toReal());
+ audioOutput->setVolume(settings.value("volume", 1.).toReal());
// audioOutput->setMuted(settings.value("volumeMute").toBool());
+
+ mediaObject->stop();
}
#endif
void MainWindow::volumeMute() {
#ifdef APP_PHONON
- volumeSlider->audioOutput()->setMuted(!volumeSlider->audioOutput()->isMuted());
+ bool muted = volumeSlider->audioOutput()->isMuted();
+ volumeSlider->audioOutput()->setMuted(!muted);
+ qApp->processEvents();
+ if (muted && volumeSlider->audioOutput()->volume() == 0) {
+ volumeSlider->audioOutput()->setVolume(volumeSlider->maximumVolume());
+ }
+ qDebug() << volumeSlider->audioOutput()->isMuted() << volumeSlider->audioOutput()->volume();
#endif
}
void MainWindow::volumeChanged(qreal newVolume) {
#ifdef APP_PHONON
// automatically unmute when volume changes
- if (volumeSlider->audioOutput()->isMuted())
- volumeSlider->audioOutput()->setMuted(false);
+ if (volumeSlider->audioOutput()->isMuted()) volumeSlider->audioOutput()->setMuted(false);
bool isZero = volumeSlider->property("zero").toBool();
bool styleChanged = false;
volumeMuteAct->setIcon(IconUtils::icon("audio-volume-high"));
statusBar()->showMessage(tr("Volume is unmuted"));
}
+#ifdef Q_WS_X11
+ QToolButton *volumeMuteButton = qobject_cast<QToolButton *>(mainToolBar->widgetForAction(volumeMuteAct));
+ volumeMuteButton->setIcon(volumeMuteButton->icon().pixmap(16));
+#endif
}
void MainWindow::setDefinitionMode(QString definitionName) {
#include "ytsinglevideosource.h"
#include "channelaggregator.h"
#include "iconutils.h"
-#include "ytuser.h"
+#include "ytchannel.h"
#ifdef APP_SNAPSHOT
#include "snapshotsettings.h"
#endif
}
}
}
- setVideoSource(new YTSearch(searchParams, this));
+ YTSearch *ytSearch = new YTSearch(searchParams, this);
+ ytSearch->setAsyncDetails(true);
+ connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
+ setVideoSource(ytSearch);
}
void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
sidebar->getHeader()->updateInfo();
SearchParams *searchParams = getSearchParams();
- bool isChannel = searchParams && !searchParams->author().isEmpty();
+ bool isChannel = searchParams && !searchParams->channelId().isEmpty();
playlistView->setClickableAuthors(!isChannel);
if (newState == Phonon::PlayingState)
videoAreaWidget->showVideo();
else if (newState == Phonon::ErrorState) {
- qDebug() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
+ qWarning() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
if (mediaObject->errorType() == Phonon::FatalError)
handleError(mediaObject->errorString());
}
a->setEnabled(enableDownload);
a->setVisible(enableDownload);
- updateSubscriptionAction(video, YTUser::isSubscribed(video->userId()));
+ updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
foreach (QAction *action, currentVideoActions)
action->setEnabled(true);
startDownloading();
#endif
- // ensure we always have 10 videos ahead
+ // ensure we always have videos ahead
playlistModel->searchNeeded();
// ensure active item is visible
#endif
#ifdef APP_EXTRA
- Extra::notify(video->title(), video->author(), video->formattedDuration());
+ Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
#endif
ChannelAggregator::instance()->videoWatched(video);
void MediaView::copyWebPage() {
Video* video = playlistModel->activeVideo();
if (!video) return;
- QString address = video->webpage().toString();
+ QString address = video->webpage();
QApplication::clipboard()->setText(address);
QString message = tr("You can now paste the YouTube link into another application");
MainWindow::instance()->showMessage(message);
if (!video) return;
QString location = SnapshotSettings::getCurrentLocation();
+ QDir dir(location);
+ if (!dir.exists()) dir.mkpath(location);
QString basename = video->title();
QString format = video->duration() > 3600 ? "h_mm_ss" : "m_ss";
basename += " (" + QTime().addSecs(currentTime).toString(format) + ")";
SearchParams *searchParams = new SearchParams();
searchParams->setTransient(true);
searchParams->setKeywords(query);
- searchParams->setAuthor(video->author());
+ searchParams->setChannelId(video->channelId());
/*
if (!numberAsWords) {
Video* video = playlistModel->activeVideo();
if (!video) return;
YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
- singleVideoSource->setVideoId(video->id());
+ singleVideoSource->setVideo(video->clone());
+ singleVideoSource->setAsyncDetails(true);
setVideoSource(singleVideoSource);
The::globalActions()->value("related-videos")->setEnabled(false);
}
#endif
url.addQueryItem("via", "minitubeapp");
url.addQueryItem("text", video->title());
- url.addQueryItem("url", video->webpage().toString());
+ url.addQueryItem("url", video->webpage());
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
QUrlQuery url;
#endif
url.addQueryItem("t", video->title());
- url.addQueryItem("u", video->webpage().toString());
+ url.addQueryItem("u", video->webpage());
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
#endif
url.addQueryItem("via", "minitubeapp");
url.addQueryItem("text", video->title());
- url.addQueryItem("url", video->webpage().toString());
+ url.addQueryItem("url", video->webpage());
url.addQueryItem("picture", video->thumbnailUrl());
#if QT_VERSION >= 0x050000
u.setQuery(url);
#endif
url.addQueryItem("subject", video->title());
QString body = video->title() + "\n" +
- video->webpage().toString() + "\n\n" +
+ video->webpage() + "\n\n" +
tr("Sent from %1").arg(Constants::NAME) + "\n" +
Constants::WEBSITE;
url.addQueryItem("body", body);
Video* video = playlistModel->videoAt(index.row());
if (!video) return;
- QString channel = video->userId();
- if (channel.isEmpty()) channel = video->author();
- if (channel.isEmpty()) return;
+ QString channelId = video->channelId();
+ // if (channelId.isEmpty()) channelId = video->channelTitle();
+ if (channelId.isEmpty()) return;
SearchParams *searchParams = new SearchParams();
- searchParams->setAuthor(channel);
+ searchParams->setChannelId(channelId);
searchParams->setSortBy(SearchParams::SortByNewest);
// go!
subscribeText = subscribeAction->property("originalText").toString();
subscribeAction->setEnabled(false);
} else if (subscribed) {
- subscribeText = tr("Unsubscribe from %1").arg(video->author());
+ subscribeText = tr("Unsubscribe from %1").arg(video->channelTitle());
subscribeTip = subscribeText;
subscribeAction->setEnabled(true);
} else {
- subscribeText = tr("Subscribe to %1").arg(video->author());
+ subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
subscribeTip = subscribeText;
subscribeAction->setEnabled(true);
}
void MediaView::toggleSubscription() {
Video *video = playlistModel->activeVideo();
if (!video) return;
- QString userId = video->userId();
+ QString userId = video->channelId();
if (userId.isEmpty()) return;
- bool subscribed = YTUser::isSubscribed(userId);
- if (subscribed) YTUser::unsubscribe(userId);
- else YTUser::subscribe(userId);
+ bool subscribed = YTChannel::isSubscribed(userId);
+ if (subscribed) YTChannel::unsubscribe(userId);
+ else YTChannel::subscribe(userId);
updateSubscriptionAction(video, !subscribed);
}
setupReply();
readTimeoutTimer->start();
return;
- } else qWarning() << "Redirection not supported" << networkReply->url().toEncoded();
+ } else qDebug() << "Redirection not supported" << networkReply->url().toEncoded();
}
if (receivers(SIGNAL(data(QByteArray))) > 0)
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#include "paginatedvideosource.h"
+
+#include "yt3.h"
+#include "yt3listparser.h"
+#include "datautils.h"
+
+#include "video.h"
+#include "networkaccess.h"
+
+namespace The {
+NetworkAccess* http();
+QHash<QString, QAction*>* globalActions();
+}
+
+PaginatedVideoSource::PaginatedVideoSource(QObject *parent) : VideoSource(parent)
+ , tokenTimestamp(0)
+ , reloadingToken(false)
+ , currentMax(0)
+ , currentStartIndex(0)
+ , asyncDetails(false) { }
+
+bool PaginatedVideoSource::hasMoreVideos() {
+ qDebug() << __PRETTY_FUNCTION__ << nextPageToken;
+ return !nextPageToken.isEmpty();
+}
+
+bool PaginatedVideoSource::maybeReloadToken(int max, int startIndex) {
+ // kind of hackish. Thank the genius who came up with this stateful stuff
+ // in a supposedly RESTful (aka stateless) API.
+
+ if (nextPageToken.isEmpty()) {
+ // previous request did not return a page token. Game over.
+ // emit gotVideos(QList<Video*>());
+ emit finished(0);
+ return true;
+ }
+
+ if (isPageTokenExpired()) {
+ reloadingToken = true;
+ currentMax = max;
+ currentStartIndex = startIndex;
+ reloadToken();
+ return true;
+ }
+ return false;
+}
+
+bool PaginatedVideoSource::setPageToken(const QString &value) {
+ tokenTimestamp = QDateTime::currentDateTime().toTime_t();
+ nextPageToken = value;
+
+ if (reloadingToken) {
+ reloadingToken = false;
+ loadVideos(currentMax, currentStartIndex);
+ currentMax = currentStartIndex = 0;
+ return true;
+ }
+
+ return false;
+}
+
+bool PaginatedVideoSource::isPageTokenExpired() {
+ uint now = QDateTime::currentDateTime().toTime_t();
+ return now - tokenTimestamp > 1800;
+}
+
+void PaginatedVideoSource::reloadToken() {
+ qDebug() << "Reloading pageToken";
+ QObject *reply = The::http()->get(lastUrl);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void PaginatedVideoSource::loadVideoDetails(const QList<Video*> &videos) {
+ QString videoIds;
+ foreach (Video *video, videos) {
+ // TODO get video details from cache
+ if (!videoIds.isEmpty()) videoIds += ",";
+ videoIds += video->id();
+ videoMap.insert(video->id(), video);
+ }
+
+ if (videoIds.isEmpty()) {
+ if (!asyncDetails) {
+ emit gotVideos(videos);
+ emit finished(videos.size());
+ }
+ return;
+ }
+
+ QUrl url = YT3::instance().method("videos");
+
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+
+ url.addQueryItem("part", "contentDetails,statistics");
+ url.addQueryItem("id", videoIds);
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseVideoDetails(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void PaginatedVideoSource::parseVideoDetails(const QByteArray &bytes) {
+
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+
+ QScriptValue items = json.property("items");
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ if (!item.isObject()) continue;
+
+ // qDebug() << item.toString();
+
+ QString id = item.property("id").toString();
+ Video *video = videoMap.value(id);
+ if (!video) {
+ qWarning() << "No video for id" << id;
+ continue;
+ }
+
+ QString isoPeriod = item.property("contentDetails").property("duration").toString();
+ int duration = DataUtils::parseIsoPeriod(isoPeriod);
+ video->setDuration(duration);
+
+ uint viewCount = item.property("statistics").property("viewCount").toUInt32();
+ video->setViewCount(viewCount);
+
+ // TODO cache by etag?
+ }
+ }
+ if (!asyncDetails) {
+ emit gotVideos(videoMap.values());
+ emit finished(videoMap.size());
+ } else {
+ emit gotDetails();
+ }
+}
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#ifndef PAGINATEDVIDEOSOURCE_H
+#define PAGINATEDVIDEOSOURCE_H
+
+#include "videosource.h"
+
+class PaginatedVideoSource : public VideoSource {
+
+ Q_OBJECT
+
+public:
+ PaginatedVideoSource(QObject *parent = 0);
+ virtual bool hasMoreVideos();
+
+ bool maybeReloadToken(int max, int startIndex);
+ bool setPageToken(const QString &value);
+ bool isPageTokenExpired();
+ void reloadToken();
+ void setAsyncDetails(bool value) { asyncDetails = value; }
+ void loadVideoDetails(const QList<Video*> &videos);
+
+signals:
+ void gotDetails();
+
+protected slots:
+ void parseVideoDetails(const QByteArray &bytes);
+
+protected:
+ QString nextPageToken;
+ uint tokenTimestamp;
+ QUrl lastUrl;
+ int currentMax;
+ int currentStartIndex;
+ bool reloadingToken;
+ QHash<QString, Video*> videoMap;
+ bool asyncDetails;
+
+};
+
+#endif // PAGINATEDVIDEOSOURCE_H
void PainterUtils::centeredMessage(QString message, QWidget* widget) {
QPainter painter(widget);
- painter.setFont(FontUtils::bigBold());
+ painter.setFont(FontUtils::big());
QSize textSize(QFontMetrics(painter.font()).size(Qt::TextSingleLine, message));
QPoint topLeft(
(widget->width()-textSize.width())/2,
painter->drawPixmap(playIcon.rect(), playIcon);
// time
- drawTime(painter, video->formattedDuration(), line);
+ if (video->duration() > 0)
+ drawTime(painter, video->formattedDuration(), line);
// separator
painter->setPen(option.palette.color(QPalette::Midlight));
if (line.width() > THUMB_WIDTH + 60) {
- if (isActive) painter->setFont(boldFont);
+ // if (isActive) painter->setFont(boldFont);
// text color
if (isSelected)
else
painter->setOpacity(.5);
}
- QString authorString = video->author();
+ QString authorString = video->channelTitle();
textLoc.setX(textLoc.x() + stringSize.width() + PADDING);
stringSize = QSize(QFontMetrics(painter->font()).size( Qt::TextSingleLine, authorString ) );
QRect authorTextBox(textLoc , stringSize);
} else {
- bool isHovered = option.state & QStyle::State_MouseOver;
+ const bool isHovered = index.data(HoveredItemRole).toBool();
if (!isActive && isHovered) {
painter->setFont(smallerFont);
painter->setPen(Qt::white);
#include "searchparams.h"
#include "mediaview.h"
-static const int maxItems = 10;
+static const int maxItems = 50;
static const QString recentKeywordsKey = "recentKeywords";
static const QString recentChannelsKey = "recentChannels";
firstSearch = false;
m_activeVideo = 0;
m_activeRow = -1;
- skip = 1;
+ startIndex = 1;
max = 0;
hoveredRow = -1;
authorHovered = false;
case Qt::DisplayRole:
if (!errorMessage.isEmpty()) return errorMessage;
if (searching) return tr("Searching...");
- if (canSearchMore) return tr("Show %1 More").arg(maxItems);
+ if (canSearchMore) return tr("Show %1 More").arg("").simplified();
if (videos.isEmpty()) return tr("No videos");
else return tr("No more videos");
case Qt::TextAlignmentRole:
void PlaylistModel::setVideoSource(VideoSource *videoSource) {
beginResetModel();
- while (!videos.isEmpty())
- delete videos.takeFirst();
+ while (!videos.isEmpty()) delete videos.takeFirst();
+ videos.clear();
m_activeVideo = 0;
m_activeRow = -1;
- skip = 1;
+ startIndex = 1;
endResetModel();
this->videoSource = videoSource;
void PlaylistModel::searchMore(int max) {
if (searching) return;
searching = true;
- firstSearch = skip == 1;
+ firstSearch = startIndex == 1;
this->max = max;
errorMessage.clear();
- videoSource->loadVideos(max, skip);
- skip += max;
+ videoSource->loadVideos(max, startIndex);
+ startIndex += max;
}
void PlaylistModel::searchMore() {
}
void PlaylistModel::searchNeeded() {
+ const int desiredRowsAhead = 10;
int remainingRows = videos.size() - m_activeRow;
- int rowsNeeded = maxItems - remainingRows;
- if (rowsNeeded > 0)
- searchMore(rowsNeeded);
+ if (remainingRows < desiredRowsAhead)
+ searchMore(maxItems);
}
void PlaylistModel::abortSearch() {
+ QMutexLocker locker(&mutex);
beginResetModel();
- while (!videos.isEmpty())
- delete videos.takeFirst();
+ // while (!videos.isEmpty()) delete videos.takeFirst();
// if (videoSource) videoSource->abort();
+ videos.clear();
searching = false;
m_activeRow = -1;
m_activeVideo = 0;
- skip = 1;
+ startIndex = 1;
endResetModel();
}
void PlaylistModel::searchFinished(int total) {
+ qDebug() << __PRETTY_FUNCTION__ << total;
searching = false;
- canSearchMore = total >= max;
+ canSearchMore = videoSource->hasMoreVideos();
// update the message item
emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
endInsertRows();
foreach (Video* video, newVideos) {
connect(video, SIGNAL(gotThumbnail()),
- SLOT(updateThumbnail()), Qt::UniqueConnection);
+ SLOT(updateVideoSender()), Qt::UniqueConnection);
video->loadThumbnail();
+ qApp->processEvents();
}
}
}
// save channel
- QString channel = searchParams->author();
- if (!channel.isEmpty() && !searchParams->isTransient()) {
+ QString channelId = searchParams->channelId();
+ if (!channelId.isEmpty() && !searchParams->isTransient()) {
QString value;
- if (!video->userId().isEmpty() && video->userId() != video->author())
- value = video->userId() + "|" + video->author();
- else value = video->author();
+ if (!video->channelId().isEmpty() && video->channelId() != video->channelTitle())
+ value = video->channelId() + "|" + video->channelTitle();
+ else value = video->channelTitle();
QStringList channels = settings.value(recentChannelsKey).toStringList();
channels.removeAll(value);
- channels.removeAll(channel);
+ channels.removeAll(channelId);
channels.prepend(value);
while (channels.size() > maxRecentElements)
channels.removeLast();
}
}
-void PlaylistModel::updateThumbnail() {
-
+void PlaylistModel::updateVideoSender() {
Video *video = static_cast<Video *>(sender());
if (!video) {
qDebug() << "Cannot get sender";
return;
}
-
int row = rowForVideo(video);
emit dataChanged( createIndex( row, 0 ), createIndex( row, columnCount() - 1 ) );
+}
+void PlaylistModel::emitDataChanged() {
+ QModelIndex index = createIndex(rowCount()-1, 0);
+ emit dataChanged(index, index);
}
// --- item removal
Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
if (index.isValid()) {
if (index.row() == videos.size()) {
- // don't drag the "show 10 more" item
+ // don't drag the "show more" item
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
} else return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
}
}
int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
+ if (videoId.isEmpty()) return -1;
for (int i = 0; i < videos.size(); ++i) {
Video *v = videos.at(i);
+ // qDebug() << "Comparing" << v->id() << videoId;
if (v->id() == videoId) return i;
}
return -1;
}
void PlaylistModel::clearHover() {
- emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+ int oldRow = hoveredRow;
hoveredRow = -1;
+ emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1) );
+}
+
+void PlaylistModel::updateHoveredRow() {
+ emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
}
/* clickable author */
void PlaylistModel::enterAuthorHover() {
if (authorHovered) return;
authorHovered = true;
- updateAuthor();
+ updateHoveredRow();
}
void PlaylistModel::exitAuthorHover() {
if (!authorHovered) return;
authorHovered = false;
- updateAuthor();
+ updateHoveredRow();
setHoveredRow(hoveredRow);
}
void PlaylistModel::enterAuthorPressed() {
if (authorPressed) return;
authorPressed = true;
- updateAuthor();
+ updateHoveredRow();
}
void PlaylistModel::exitAuthorPressed() {
if (!authorPressed) return;
authorPressed = false;
- updateAuthor();
-}
-
-void PlaylistModel::updateAuthor() {
- emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
+ updateHoveredRow();
}
void addVideos(QList<Video*> newVideos);
void searchFinished(int total);
void searchError(QString message);
- void updateThumbnail();
+ void updateVideoSender();
+ void emitDataChanged();
void setHoveredRow(int row);
void clearHover();
+ void updateHoveredRow();
+
void enterAuthorHover();
void exitAuthorHover();
void enterAuthorPressed();
void exitAuthorPressed();
- void updateAuthor();
signals:
void activeRowChanged(int);
bool firstSearch;
QList<Video*> videos;
- int skip;
+ int startIndex;
int max;
int m_activeRow;
int hoveredRow;
bool authorHovered;
bool authorPressed;
+
+ QMutex mutex;
};
#endif
if (listModel) listModel->setHoveredRow(index.row());
}
-void PlaylistView::leaveEvent(QEvent * /* event */) {
+void PlaylistView::leaveEvent(QEvent *event) {
+ QListView::leaveEvent(event);
PlaylistModel *listModel = dynamic_cast<PlaylistModel *>(model());
if (listModel) listModel->clearHover();
}
m_duration = DurationAny;
m_quality = QualityAny;
m_time = TimeAny;
+ m_publishedAfter = 0;
}
void SearchParams::setParam(QString name, QVariant value) {
SearchParams(QObject *parent = 0);
const QString keywords() const { return m_keywords; }
- void setKeywords( QString keywords ) { m_keywords = keywords; }
+ void setKeywords(const QString &keywords) { m_keywords = keywords; }
- const QString author() const { return m_author; }
- void setAuthor( QString author ) { m_author = author; }
+ const QString channelId() const { return m_channelId; }
+ void setChannelId(const QString &value) { m_channelId = value; }
int sortBy() const { return m_sortBy; }
void setSortBy( int sortBy ) { m_sortBy = sortBy; }
int time() const { return m_time; }
void setTime( int time ) { m_time = time; }
+ uint publishedAfter() const { return m_publishedAfter; }
+ void setPublishedAfter(uint value) { m_publishedAfter = value; }
+
bool operator==(const SearchParams &other) const {
return m_keywords == other.keywords() &&
- m_author == other.author();
+ m_channelId == other.channelId();
}
public slots:
private:
QString m_keywords;
- QString m_author;
+ QString m_channelId;
bool m_transient;
int m_sortBy;
int m_duration;
int m_quality;
int m_time;
+ uint m_publishedAfter;
};
#if defined(APP_UBUNTU) || defined(APP_WIN)
"normal"
#else
- "bold"
+ "normal"
#endif
"' ")
.arg(Constants::WEBSITE, Constants::NAME)
}
void SearchView::appear() {
+ setUpdatesEnabled(false);
updateRecentKeywords();
updateRecentChannels();
queryEdit->selectAll();
queryEdit->enableSuggest();
if (!queryEdit->hasFocus())
QTimer::singleShot(10, queryEdit, SLOT(setFocus()));
+ setUpdatesEnabled(true);
}
void SearchView::updateRecentKeywords() {
else {
// remove spaces from channel name
query = query.simplified();
- searchParams->setAuthor(query);
+ query = query.remove(' ');
+ searchParams->setChannelId(query);
searchParams->setSortBy(SearchParams::SortByNewest);
}
emit search(searchParams);
}
-void SearchView::watchChannel(QString channel) {
-
- channel = channel.simplified();
-
- // check for empty query
- if (channel.length() == 0) {
+void SearchView::watchChannel(const QString &channelId) {
+ if (channelId.length() == 0) {
queryEdit->setFocus(Qt::OtherFocusReason);
return;
}
- // remove spaces from channel name
- channel = channel.remove(" ");
+ QString id = channelId;
+
+ // Fix old settings
+ if (!id.startsWith("UC")) id = "UC" + id;
SearchParams *searchParams = new SearchParams();
- searchParams->setAuthor(channel);
+ searchParams->setChannelId(id);
searchParams->setSortBy(SearchParams::SortByNewest);
// go!
}
void SearchView::suggestionAccepted(Suggestion *suggestion) {
- watch(suggestion->value);
+ if (suggestion->type == QLatin1String("channel")) {
+ watchChannel(suggestion->userData);
+ } else watch(suggestion->value);
}
void appear();
void disappear() { }
void watch(QString query);
- void watchChannel(QString channel);
+ void watchChannel(const QString &channelId);
void watchKeywords(QString query);
signals:
#ifdef APP_MAC
#include "macutils.h"
#endif
+#include "constants.h"
SnapshotSettings::SnapshotSettings(QWidget *parent) : QWidget(parent) {
QBoxLayout *layout = new QHBoxLayout(this);
QString display = displayPath(path);
QString msg = tr("Snapshot saved to %1")
- .arg("<a href='showFile' style='text-decoration:none; color:palette(text); font-weight:bold'>%1</a>")
- .arg(display);
+ .arg("<a href='showFile' style='text-decoration:none; color:palette(text); font-weight:bold'>%1</a>")
+ .arg(display);
message->setText(msg);
}
location = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
#else
location = QDesktopServices::storageLocation(QDesktopServices::PicturesLocation);
+#endif
+#ifdef APP_MAC_STORE
+ location += "/MinitubeforYouTube";
#endif
}
return location;
}
void StandardFeedsView::load() {
+ setUpdatesEnabled(false);
YTCategories *youTubeCategories = new YTCategories(this);
connect(youTubeCategories, SIGNAL(categoriesLoaded(const QList<YTCategory> &)),
SLOT(layoutCategories(const QList<YTCategory> &)));
feed->setFeedId("most_popular");
addVideoSourceWidget(feed);
}
+ if (categories.size() > 1) setUpdatesEnabled(true);
}
void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
void StandardFeedsView::appear() {
setFocus();
- if (!layout) load();
+ if (!layout) {
+ update();
+ qApp->processEvents();
+ load();
+ }
QAction *regionAction = MainWindow::instance()->getRegionAction();
regionAction->setVisible(true);
}
void StandardFeedsView::paintEvent(QPaintEvent *event) {
QWidget::paintEvent(event);
- PainterUtils::topShadow(this);
+ // PainterUtils::topShadow(this);
}
public:
Suggestion(QString value = QString(),
- QString type = QString()) :
- value(value), type(type) { }
+ QString type = QString(),
+ QString userData = QString()) :
+ value(value), type(type), userData(userData) { }
QString value;
QString type;
+ QString userData;
bool operator==(const Suggestion &other) const {
return (value == other.value) && (type == other.type);
elIndex(0),
ageGate(false),
loadingStreamUrl(false),
- loadingThumbnail(false)
-{ }
+ loadingThumbnail(false) {
+}
Video* Video::clone() {
Video* cloneVideo = new Video();
cloneVideo->m_title = m_title;
cloneVideo->m_description = m_description;
- cloneVideo->m_author = m_author;
- cloneVideo->m_userId = m_userId;
+ cloneVideo->m_channelTitle = m_channelTitle;
+ cloneVideo->m_channelId = m_channelId;
cloneVideo->m_webpage = m_webpage;
cloneVideo->m_streamUrl = m_streamUrl;
cloneVideo->m_thumbnail = m_thumbnail;
return cloneVideo;
}
-void Video::setWebpage(QUrl webpage) {
- m_webpage = webpage;
+const QString &Video::webpage() {
+ if (m_webpage.isEmpty() && !videoId.isEmpty())
+ m_webpage.append("https://www.youtube.com/watch?v=").append(videoId);
+ return m_webpage;
+}
+
+void Video::setWebpage(const QString &value) {
+ m_webpage = value;
// Get Video ID
- QRegExp re(JsFunctions::instance()->videoIdRE());
- if (re.indexIn(m_webpage.toString()) == -1) {
- qWarning() << QString("Cannot get video id for %1").arg(m_webpage.toString());
- // emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
- // loadingStreamUrl = false;
- return;
+ if (videoId.isEmpty()) {
+ QRegExp re(JsFunctions::instance()->videoIdRE());
+ if (re.indexIn(m_webpage) == -1) {
+ qWarning() << QString("Cannot get video id for %1").arg(m_webpage);
+ // emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
+ // loadingStreamUrl = false;
+ return;
+ }
+ videoId = re.cap(1);
}
- videoId = re.cap(1);
}
void Video::loadThumbnail() {
void Video::setThumbnail(QByteArray bytes) {
loadingThumbnail = false;
+ m_thumbnail = QPixmap();
m_thumbnail.loadFromData(bytes);
if (m_thumbnail.width() > 160)
m_thumbnail = m_thumbnail.scaledToWidth(160, Qt::SmoothTransformation);
if (elIndex == elTypes.size()) {
// qDebug() << "Trying special embedded el param";
- url = QUrl("http://www.youtube.com/get_video_info");
+ url = QUrl("https://www.youtube.com/get_video_info");
#if QT_VERSION >= 0x050000
{
} else {
// qDebug() << "Trying el param:" << elTypes.at(elIndex) << elIndex;
url = QUrl(QString(
- "http://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
+ "https://www.youtube.com/get_video_info?video_id=%1%2&ps=default&eurl=&gl=US&hl=en"
).arg(videoId, elTypes.at(elIndex)));
}
// get video token
QRegExp videoTokeRE(JsFunctions::instance()->videoTokenRE());
if (videoTokeRE.indexIn(videoInfo) == -1) {
- // qWarning() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
+ qDebug() << "Cannot get token. Trying next el param" << videoInfo << videoTokeRE.pattern();
// Don't panic! We're gonna try another magic "el" param
elIndex++;
getVideoInfo();
}
QString videoToken = videoTokeRE.cap(1);
- // qWarning() << "got token" << videoToken;
+ // qDebug() << "got token" << videoToken;
while (videoToken.contains('%'))
videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
// qDebug() << "videoToken" << videoToken;
// get fmt_url_map
QRegExp fmtMapRE(JsFunctions::instance()->videoInfoFmtMapRE());
if (fmtMapRE.indexIn(videoInfo) == -1) {
- // qWarning() << "Cannot get urlMap. Trying next el param";
+ // qDebug() << "Cannot get urlMap. Trying next el param";
// Don't panic! We're gonna try another magic "el" param
elIndex++;
getVideoInfo();
// qDebug() << "Got token and urlMap" << elIndex;
QString fmtUrlMap = fmtMapRE.cap(1);
- // qWarning() << "got fmtUrlMap" << fmtUrlMap;
+ // qDebug() << "got fmtUrlMap" << fmtUrlMap;
fmtUrlMap = QByteArray::fromPercentEncoding(fmtUrlMap.toUtf8());
parseFmtUrlMap(fmtUrlMap);
}
sig = JsFunctions::instance()->decryptSignature(sig);
}
} else {
- // qDebug() << "Loading webpage";
+
QUrl url("http://www.youtube.com/watch");
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
#endif
+ // qDebug() << "Loading webpage" << url;
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(scrapeWebPage(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
// qWarning() << url;
if (format == definitionCode) {
- qDebug() << "Found format" << definitionCode;
+ // qDebug() << "Found format" << definitionCode;
QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
m_streamUrl = videoUrl;
this->definitionCode = definitionCode;
if (previousIndex < 0) previousIndex = 0;
int definitionCode = definitionCodes.at(previousIndex);
if (urlMap.contains(definitionCode)) {
- qDebug() << "Found format" << definitionCode;
+ // qDebug() << "Found format" << definitionCode;
QString url = urlMap.value(definitionCode);
QUrl videoUrl = QUrl::fromEncoded(url.toUtf8(), QUrl::StrictMode);
m_streamUrl = videoUrl;
currentIndex--;
}
- emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage.toString()));
+ emit errorStreamUrl(tr("Cannot get video stream for %1").arg(m_webpage));
}
void Video::errorVideoInfo(QNetworkReply *reply) {
void Video::scrapeWebPage(QByteArray data) {
QString html = QString::fromUtf8(data);
- // qWarning() << html;
QRegExp ageGateRE(JsFunctions::instance()->ageGateRE());
if (ageGateRE.indexIn(html) != -1) {
QRegExp fmtMapRE(JsFunctions::instance()->webPageFmtMapRE());
if (fmtMapRE.indexIn(html) == -1) {
- // qWarning() << "Error parsing video page";
+ qWarning() << "Error parsing video page";
// emit errorStreamUrl("Error parsing video page");
// loadingStreamUrl = false;
elIndex++;
#ifdef APP_DASH
QSettings settings;
QString definitionName = settings.value("definition", "360p").toString();
- if (definitionName == QLatin1String("1080p") {
+ if (definitionName == QLatin1String("1080p")) {
QRegExp dashManifestRe("\"dashmpd\":\\s*\"([^\"]+)\"");
if (dashManifestRe.indexIn(html) != -1) {
dashManifestUrl = dashManifestRe.cap(1);
jsPlayerUrl = "http:" + jsPlayerUrl;
// qDebug() << "jsPlayerUrl" << jsPlayerUrl;
/*
- QRegExp jsPlayerIdRe("-(.+)\\.js");
- jsPlayerIdRe.indexIn(jsPlayerUrl);
- QString jsPlayerId = jsPlayerRe.cap(1);
- */
+ QRegExp jsPlayerIdRe("-(.+)\\.js");
+ jsPlayerIdRe.indexIn(jsPlayerUrl);
+ QString jsPlayerId = jsPlayerRe.cap(1);
+ */
QObject *reply = The::http()->get(jsPlayerUrl);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseJsPlayer(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
dashManifestUrl.replace(sigRe, "/signature/" + sig);
qDebug() << dashManifestUrl;
- m_streamUrl = dashManifestUrl;
- this->definitionCode = 37;
- emit gotStreamUrl(m_streamUrl);
- loadingStreamUrl = false;
-
- /*
- QObject *reply = The::http()->get(QUrl::fromEncoded(dashManifestUrl.toUtf8()));
- connect(reply, SIGNAL(data(QByteArray)), SLOT(parseDashManifest(QByteArray)));
- connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
- */
+ if (false) {
+ // let phonon play the manifest
+ m_streamUrl = dashManifestUrl;
+ this->definitionCode = 37;
+ emit gotStreamUrl(m_streamUrl);
+ loadingStreamUrl = false;
+ } else {
+ // download the manifest
+ QObject *reply = The::http()->get(QUrl::fromEncoded(dashManifestUrl.toUtf8()));
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseDashManifest(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(errorVideoInfo(QNetworkReply*)));
+ }
return;
}
}
void Video::parseDashManifest(QByteArray bytes) {
- QFile file(Temporary::filename());
+ QFile file(Temporary::filename() + ".mpd");
if (!file.open(QIODevice::WriteOnly))
qWarning() << file.errorString() << file.fileName();
QDataStream stream(&file);
};
const QString & title() const { return m_title; }
- void setTitle( QString title ) { m_title = title; }
+ void setTitle(const QString &title) { m_title = title; }
const QString & description() const { return m_description; }
- void setDescription( QString description ) { m_description = description; }
+ void setDescription(const QString &description) { m_description = description; }
- const QString & author() const { return m_author; }
- void setAuthor( QString author ) { m_author = author; }
+ const QString & channelTitle() const { return m_channelTitle; }
+ void setChannelTitle(const QString &value) { m_channelTitle = value; }
- const QString & userId() const { return m_userId; }
- void setUserId( QString userId ) { m_userId = userId; }
+ const QString & channelId() const { return m_channelId; }
+ void setChannelId(const QString &value ) { m_channelId = value; }
- const QUrl & webpage() const { return m_webpage; }
- void setWebpage(QUrl webpage);
+ const QString & webpage();
+ void setWebpage(const QString &value);
void loadThumbnail();
const QPixmap & thumbnail() const { return m_thumbnail; }
const QString & thumbnailUrl() { return m_thumbnailUrl; }
- void setThumbnailUrl(QString url) { m_thumbnailUrl = url; }
+ void setThumbnailUrl(const QString &url) { m_thumbnailUrl = url; }
void loadMediumThumbnail();
const QString & mediumThumbnailUrl() { return m_mediumThumbnailUrl; }
- void setMediumThumbnailUrl(QString url) { m_mediumThumbnailUrl = url; }
+ void setMediumThumbnailUrl(const QString &url) { m_mediumThumbnailUrl = url; }
int duration() const { return m_duration; }
void setDuration( int duration ) { m_duration = duration; }
void setViewCount( int viewCount ) { m_viewCount = viewCount; }
const QDateTime & published() const { return m_published; }
- void setPublished( QDateTime published ) { m_published = published; }
+ void setPublished(const QDateTime &published ) { m_published = published; }
int getDefinitionCode() const { return definitionCode; }
QString m_title;
QString m_description;
- QString m_author;
- QString m_userId;
- QUrl m_webpage;
+ QString m_channelTitle;
+ QString m_channelId;
+ QString m_webpage;
QUrl m_streamUrl;
QPixmap m_thumbnail;
QString m_thumbnailUrl;
public:
VideoSource(QObject *parent = 0) : QObject(parent) { }
- virtual void loadVideos(int max, int skip) = 0;
+ virtual void loadVideos(int max, int startIndex) = 0;
+ virtual bool hasMoreVideos() { return true; }
virtual void abort() = 0;
virtual const QStringList & getSuggestions() = 0;
virtual QString getName() = 0;
--- /dev/null
+#include "yt3.h"
+
+#include <algorithm>
+#include <ctime>
+
+#include "jsfunctions.h"
+#include "networkaccess.h"
+#include "constants.h"
+
+#ifdef APP_EXTRA
+#include "extra.h"
+#endif
+
+#define STR(x) #x
+#define STRINGIFY(x) STR(x)
+
+namespace The {
+NetworkAccess* http();
+}
+
+YT3 &YT3::instance() {
+ static YT3 *i = new YT3();
+ return *i;
+}
+
+YT3::YT3() {
+ QByteArray customApiKey = qgetenv("GOOGLE_API_KEY");
+ if (!customApiKey.isEmpty()) {
+ keys << QString::fromUtf8(customApiKey);
+ qDebug() << "API key from environment" << keys;
+ }
+
+ if (keys.isEmpty()) {
+ QSettings settings;
+ if (settings.contains("googleApiKey")) {
+ keys << settings.value("googleApiKey").toString();
+ qDebug() << "API key from settings" << keys;
+ }
+ }
+
+#ifdef APP_GOOGLE_API_KEY
+ if (keys.isEmpty()) {
+ keys << STRINGIFY(APP_GOOGLE_API_KEY);
+ qDebug() << "built-in API key" << keys;
+ }
+#endif
+
+#ifdef APP_EXTRA
+ if (keys.isEmpty())
+ keys << Extra::apiKeys();
+#endif
+
+ if (keys.isEmpty()) {
+ qWarning() << "No available API keys";
+ } else {
+ key = keys.takeFirst();
+ testApiKey();
+ }
+}
+
+const QString &YT3::baseUrl() {
+ static const QString base = "https://www.googleapis.com/youtube/v3/";
+ return base;
+}
+
+void YT3::testApiKey() {
+ QUrl url = method("videos");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("part", "id");
+ url.addQueryItem("chart", "mostPopular");
+ url.addQueryItem("maxResults", "1");
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(finished(QNetworkReply*)), SLOT(testResponse(QNetworkReply*)));
+}
+
+void YT3::addApiKey(QUrl &url) {
+ if (key.isEmpty()) {
+ qDebug() << __PRETTY_FUNCTION__ << "empty key";
+ return;
+ }
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url(u);
+#endif
+ url.addQueryItem("key", key);
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+}
+
+QUrl YT3::method(const QString &name) {
+ QUrl url(baseUrl() + name);
+ addApiKey(url);
+ return url;
+}
+
+void YT3::testResponse(QNetworkReply *reply) {
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (status != 200) {
+ if (keys.isEmpty()) {
+ qWarning() << "Fatal error: No working API keys!";
+ return;
+ }
+ key = keys.takeFirst();
+ testApiKey();
+ } else {
+ qDebug() << "Using key" << key;
+ }
+}
--- /dev/null
+#ifndef YT3_H
+#define YT3_H
+
+#include <QtCore>
+#include <QtNetwork>
+
+class YT3 : public QObject {
+
+ Q_OBJECT
+
+public:
+ static YT3 &instance();
+ static const QString &baseUrl();
+
+ void testApiKey();
+ void addApiKey(QUrl &url);
+ QUrl method(const QString &name);
+
+signals:
+ void gotChannelId(QString channelId);
+
+private slots:
+ void testResponse(QNetworkReply *reply);
+
+private:
+ YT3();
+
+ QStringList keys;
+ QString key;
+};
+
+#endif // YT3_H
--- /dev/null
+#include "yt3listparser.h"
+#include "video.h"
+#include "datautils.h"
+
+YT3ListParser::YT3ListParser(const QByteArray &bytes) {
+
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+
+ nextPageToken = json.property("nextPageToken").toString();
+
+ QScriptValue items = json.property("items");
+ videos.reserve(items.property("length").toInt32() - 1);
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ // For some reason the array has an additional element containing its size.
+ if (item.isObject()) parseItem(item);
+ }
+ }
+
+ // TODO suggestions!
+}
+
+void YT3ListParser::parseItem(const QScriptValue &item) {
+ Video *video = new Video();
+
+ QScriptValue id = item.property("id");
+ if (id.isString()) video->setId(id.toString());
+ else {
+ QString videoId = id.property("videoId").toString();
+ video->setId(videoId);
+ }
+
+ QScriptValue snippet = item.property("snippet");
+
+ bool isLiveBroadcastContent = snippet.property("liveBroadcastContent").toString() != QLatin1String("none");
+ if (isLiveBroadcastContent) {
+ delete video;
+ return;
+ }
+
+ QString publishedAt = snippet.property("publishedAt").toString();
+ QDateTime publishedDateTime = QDateTime::fromString(publishedAt, Qt::ISODate);
+ video->setPublished(publishedDateTime);
+
+ video->setChannelId(snippet.property("channelId").toString());
+
+ video->setTitle(snippet.property("title").toString());
+ video->setDescription(snippet.property("description").toString());
+
+ QScriptValue thumbnails = snippet.property("thumbnails");
+ video->setThumbnailUrl(thumbnails.property("medium").property("url").toString());
+ video->setMediumThumbnailUrl(thumbnails.property("high").property("url").toString());
+
+ video->setChannelTitle(snippet.property("channelTitle").toString());
+
+ // These are only for "videos" requests
+
+ QScriptValue contentDetails = item.property("contentDetails");
+ if (contentDetails.isObject()) {
+ QString isoPeriod = contentDetails.property("duration").toString();
+ int duration = DataUtils::parseIsoPeriod(isoPeriod);
+ video->setDuration(duration);
+ }
+
+ QScriptValue statistics = item.property("statistics");
+ if (statistics.isObject()) {
+ uint viewCount = statistics.property("viewCount").toUInt32();
+ video->setViewCount(viewCount);
+ }
+
+ videos.append(video);
+}
--- /dev/null
+#ifndef YT3LISTPARSER_H
+#define YT3LISTPARSER_H
+
+#include <QtCore>
+#include <QtScript>
+
+class Video;
+
+class YT3ListParser : public QObject {
+
+public:
+ YT3ListParser(const QByteArray &bytes);
+ const QList<Video*> &getVideos() { return videos; }
+ const QStringList &getSuggestions() { return suggestions; }
+ const QString &getNextPageToken() { return nextPageToken; }
+
+private:
+ void parseItem(const QScriptValue &item);
+
+ QList<Video*> videos;
+ QStringList suggestions;
+ QString nextPageToken;
+};
+
+#endif // YT3LISTPARSER_H
#include "ytcategories.h"
#include "networkaccess.h"
-#include <QtXml>
+#ifdef APP_YT3
+#include "datautils.h"
+#include "yt3.h"
+#include "ytregions.h"
+#include <QtScript>
+#endif
namespace The {
NetworkAccess* http();
language = QLocale::system().uiLanguages().first();
lastLanguage = language;
+#ifdef APP_YT3
+ QUrl url = YT3::instance().method("videoCategories");
+
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url(u);
+#endif
+
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("hl", language);
+
+ QString regionCode = YTRegions::currentRegionId();
+ if (regionCode.isEmpty()) regionCode = "us";
+ url.addQueryItem("regionCode", regionCode);
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+#else
QString url = "http://gdata.youtube.com/schemas/2007/categories.cat?hl=" + language;
+#endif
+
+
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseCategories(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
+#ifdef APP_YT3
+
+void YTCategories::parseCategories(QByteArray bytes) {
+ QList<YTCategory> categories;
+
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+
+ QScriptValue items = json.property("items");
+
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ // For some reason the array has an additional element containing its size.
+ if (!item.isObject()) continue;
+
+ QScriptValue snippet = item.property("snippet");
+
+ bool isAssignable = snippet.property("assignable").toBool();
+ if (!isAssignable) continue;
+
+ YTCategory category;
+ category.term = item.property("id").toString();
+ category.label = snippet.property("title").toString();
+ categories << category;
+ }
+ }
+
+ emit categoriesLoaded(categories);
+}
+
+#else
+
void YTCategories::parseCategories(QByteArray bytes) {
QList<YTCategory> categories;
emit categoriesLoaded(categories);
}
+#endif
+
void YTCategories::requestError(QNetworkReply *reply) {
if (lastLanguage != "en") loadCategories("en");
else emit error(reply->errorString());
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#include "ytchannel.h"
+#include "networkaccess.h"
+#include "database.h"
+#include <QtSql>
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include <QtScript>
+#endif
+
+namespace The {
+NetworkAccess* http();
+}
+
+YTChannel::YTChannel(const QString &channelId, QObject *parent) : QObject(parent),
+ id(0),
+ channelId(channelId),
+ loadingThumbnail(false),
+ notifyCount(0),
+ checked(0),
+ watched(0),
+ loaded(0),
+ loading(false) { }
+
+QHash<QString, YTChannel*> YTChannel::cache;
+
+YTChannel* YTChannel::forId(const QString &channelId) {
+ if (channelId.isEmpty()) return 0;
+
+ if (cache.contains(channelId))
+ return cache.value(channelId);
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
+ "from subscriptions where user_id=?");
+ query.bindValue(0, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ YTChannel* channel = 0;
+ if (query.next()) {
+ // Change userId to ChannelId
+
+ channel = new YTChannel(channelId);
+ channel->id = query.value(0).toInt();
+ channel->displayName = query.value(1).toString();
+ channel->description = query.value(2).toString();
+ channel->thumbnailUrl = query.value(3).toString();
+ channel->notifyCount = query.value(4).toInt();
+ channel->watched = query.value(5).toUInt();
+ channel->checked = query.value(6).toUInt();
+ channel->loaded = query.value(7).toUInt();
+ channel->thumbnail = QPixmap(channel->getThumbnailLocation());
+ channel->maybeLoadfromAPI();
+ cache.insert(channelId, channel);
+ }
+
+ return channel;
+}
+
+void YTChannel::maybeLoadfromAPI() {
+ if (loading) return;
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+ static const int refreshInterval = 60 * 60 * 24 * 10;
+ if (loaded > now - refreshInterval) return;
+
+ loading = true;
+
+#ifdef APP_YT3
+
+ QUrl url = YT3::instance().method("channels");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("id", channelId);
+ url.addQueryItem("part", "snippet");
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+#else
+
+ QUrl url("http://gdata.youtube.com/feeds/api/users/" + channelId);
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("v", "2");
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+
+#endif
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+#ifdef APP_YT3
+
+void YTChannel::parseResponse(const QByteArray &bytes) {
+ QScriptEngine engine;
+ QScriptValue json = engine.evaluate("(" + QString::fromUtf8(bytes) + ")");
+ QScriptValue items = json.property("items");
+ if (items.isArray()) {
+ QScriptValueIterator it(items);
+ while (it.hasNext()) {
+ it.next();
+ QScriptValue item = it.value();
+ // For some reason the array has an additional element containing its size.
+ if (item.isObject()) {
+ QScriptValue snippet = item.property("snippet");
+ displayName = snippet.property("title").toString();
+ description = snippet.property("description").toString();
+ QScriptValue thumbnails = snippet.property("thumbnails");
+ thumbnailUrl = thumbnails.property("default").property("url").toString();
+ qDebug() << displayName << description << thumbnailUrl;
+ }
+ }
+ }
+
+ emit infoLoaded();
+ storeInfo();
+ loading = false;
+}
+
+#else
+
+void YTChannel::parseResponse(const QByteArray &bytes) {
+ QXmlStreamReader xml(bytes);
+ xml.readNextStartElement();
+ if (xml.name() == QLatin1String("entry"))
+ while(xml.readNextStartElement()) {
+ const QStringRef n = xml.name();
+ if (n == QLatin1String("summary"))
+ description = xml.readElementText().simplified();
+ else if (n == QLatin1String("title"))
+ displayName = xml.readElementText();
+ else if (n == QLatin1String("thumbnail")) {
+ thumbnailUrl = xml.attributes().value("url").toString();
+ xml.skipCurrentElement();
+ } else if (n == QLatin1String("username"))
+ userName = xml.readElementText();
+ else xml.skipCurrentElement();
+ }
+
+ if (xml.hasError()) {
+ emit error(xml.errorString());
+ qWarning() << xml.errorString();
+ }
+
+ emit infoLoaded();
+ storeInfo();
+ loading = false;
+}
+
+#endif
+
+void YTChannel::loadThumbnail() {
+ if (loadingThumbnail) return;
+ if (thumbnailUrl.isEmpty()) return;
+ loadingThumbnail = true;
+
+#ifdef Q_OS_WIN
+ thumbnailUrl.replace(QLatin1String("https://"), QLatin1String("http://"));
+#endif
+
+ QUrl url(thumbnailUrl);
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+const QString & YTChannel::getThumbnailDir() {
+ static const QString thumbDir =
+ #if QT_VERSION >= 0x050000
+ QStandardPaths::writableLocation(QStandardPaths::DataLocation)
+ #else
+ QDesktopServices::storageLocation(QDesktopServices::DataLocation)
+ #endif
+ + "/channels/";
+ return thumbDir;
+}
+
+QString YTChannel::getThumbnailLocation() {
+ return getThumbnailDir() + channelId;
+}
+
+void YTChannel::unsubscribe() {
+ YTChannel::unsubscribe(channelId);
+}
+
+void YTChannel::storeThumbnail(const QByteArray &bytes) {
+ thumbnail.loadFromData(bytes);
+ static const int maxWidth = 88;
+
+ QDir dir;
+ dir.mkpath(getThumbnailDir());
+
+ if (thumbnail.width() > maxWidth) {
+ thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
+ thumbnail.save(getThumbnailLocation(), "JPG");
+ } else {
+ QFile file(getThumbnailLocation());
+ if (!file.open(QIODevice::WriteOnly))
+ qWarning() << "Error opening file for writing" << file.fileName();
+ QDataStream stream(&file);
+ stream.writeRawData(bytes.constData(), bytes.size());
+ }
+
+ emit thumbnailLoaded();
+ loadingThumbnail = false;
+}
+
+void YTChannel::requestError(QNetworkReply *reply) {
+ emit error(reply->errorString());
+ qWarning() << reply->errorString();
+ loading = false;
+ loadingThumbnail = false;
+}
+
+void YTChannel::storeInfo() {
+ if (channelId.isEmpty()) return;
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set "
+ "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
+ "where user_id=?");
+ qDebug() << userName;
+ query.bindValue(0, userName);
+ query.bindValue(1, displayName);
+ query.bindValue(2, description);
+ query.bindValue(3, thumbnailUrl);
+ query.bindValue(4, QDateTime::currentDateTime().toTime_t());
+ query.bindValue(5, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ loadThumbnail();
+}
+
+void YTChannel::subscribe(const QString &channelId) {
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("insert into subscriptions "
+ "(user_id,added,watched,checked,views,notify_count)"
+ " values (?,?,?,0,0,0)");
+ query.bindValue(0, channelId);
+ query.bindValue(1, now);
+ query.bindValue(2, now);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ // This will call maybeLoadFromApi
+ YTChannel::forId(channelId);
+}
+
+void YTChannel::unsubscribe(const QString &channelId) {
+ if (channelId.isEmpty()) return;
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("delete from subscriptions where user_id=?");
+ query.bindValue(0, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ query = QSqlQuery(db);
+ query.prepare("delete from subscriptions_videos where user_id=?");
+ query.bindValue(0, channelId);
+ success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+
+ YTChannel *user = cache.take(channelId);
+ if (user) user->deleteLater();
+}
+
+bool YTChannel::isSubscribed(const QString &channelId) {
+ if (!Database::exists()) return false;
+ if (channelId.isEmpty()) return false;
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("select count(*) from subscriptions where user_id=?");
+ query.bindValue(0, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+ if (query.next())
+ return query.value(0).toInt() > 0;
+ return false;
+}
+
+void YTChannel::updateChecked() {
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+ checked = now;
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set checked=? where user_id=?");
+ query.bindValue(0, now);
+ query.bindValue(1, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+}
+
+void YTChannel::updateWatched() {
+ if (channelId.isEmpty()) return;
+
+ uint now = QDateTime::currentDateTime().toTime_t();
+ watched = now;
+ notifyCount = 0;
+ emit notifyCountChanged();
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
+ query.bindValue(0, now);
+ query.bindValue(1, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+}
+
+void YTChannel::storeNotifyCount(int count) {
+ if (notifyCount != count)
+ emit notifyCountChanged();
+ notifyCount = count;
+
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("update subscriptions set notify_count=? where user_id=?");
+ query.bindValue(0, count);
+ query.bindValue(1, channelId);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+}
+
+bool YTChannel::updateNotifyCount() {
+ QSqlDatabase db = Database::instance().getConnection();
+ QSqlQuery query(db);
+ query.prepare("select count(*) from subscriptions_videos "
+ "where channel_id=? and added>? and published>? and watched=0");
+ query.bindValue(0, id);
+ query.bindValue(1, watched);
+ query.bindValue(2, watched);
+ bool success = query.exec();
+ if (!success) qWarning() << query.lastQuery() << query.lastError().text();
+ if (!query.next()) {
+ qWarning() << __PRETTY_FUNCTION__ << "Count failed";
+ return false;
+ }
+ int count = query.value(0).toInt();
+ storeNotifyCount(count);
+ return count != notifyCount;
+}
--- /dev/null
+/* $BEGIN_LICENSE
+
+This file is part of Minitube.
+Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
+
+Minitube is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Minitube is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Minitube. If not, see <http://www.gnu.org/licenses/>.
+
+$END_LICENSE */
+
+#ifndef YTCHANNEL_H
+#define YTCHANNEL_H
+
+#include <QtGui>
+#if QT_VERSION >= 0x050000
+#include <QtWidgets>
+#endif
+#include <QtNetwork>
+
+class YTChannel : public QObject {
+
+ Q_OBJECT
+
+public:
+ static YTChannel* forId(const QString &channelId);
+ static void subscribe(const QString &channelId);
+ static void unsubscribe(const QString &channelId);
+ static bool isSubscribed(const QString &channelId);
+
+ int getId() { return id; }
+ void setId(int id) { this->id = id; }
+
+ uint getChecked() { return checked; }
+ void updateChecked();
+
+ uint getWatched() const { return watched; }
+ void setWatched(uint watched) { this->watched = watched; }
+
+ int getNotifyCount() const { return notifyCount; }
+ void setNotifyCount(int count) { notifyCount = count; }
+ void storeNotifyCount(int count);
+ bool updateNotifyCount();
+
+ QString getChannelId() const { return channelId; }
+ QString getUserName() const { return userName; }
+ QString getDisplayName() const { return displayName; }
+ QString getDescription() const { return description; }
+ QString getCountryCode() const { return countryCode; }
+
+ void loadThumbnail();
+ const QString & getThumbnailDir();
+ QString getThumbnailLocation();
+ const QPixmap & getThumbnail() { return thumbnail; }
+
+ static QList<YTChannel*> getCachedChannels() { return cache.values(); }
+
+public slots:
+ void updateWatched();
+ void unsubscribe();
+
+signals:
+ void infoLoaded();
+ void thumbnailLoaded();
+ void error(QString message);
+ void notifyCountChanged();
+
+private slots:
+ void parseResponse(const QByteArray &bytes);
+ void requestError(QNetworkReply *reply);
+ void storeThumbnail(const QByteArray &bytes);
+
+private:
+ YTChannel(const QString &channelId, QObject *parent = 0);
+ void maybeLoadfromAPI();
+ void storeInfo();
+
+ static QHash<QString, YTChannel*> cache;
+
+ int id;
+ QString channelId;
+ QString userName;
+ QString displayName;
+ QString description;
+ QString countryCode;
+
+ QString thumbnailUrl;
+ QPixmap thumbnail;
+ bool loadingThumbnail;
+
+ int notifyCount;
+ uint checked;
+ uint watched;
+ uint loaded;
+ bool loading;
+};
+
+// This is required in order to use QPointer<YTUser> as a QVariant
+typedef QPointer<YTChannel> YTChannelPointer;
+Q_DECLARE_METATYPE(YTChannelPointer)
+
+#endif // YTCHANNEL_H
) {
QString webpage = attributes().value("href").toString();
webpage.remove("&feature=youtube_gdata");
- video->setWebpage(QUrl(webpage));
+ video->setWebpage(webpage);
} else if (name() == QLatin1String("author")) {
while(readNextStartElement())
if (name() == QLatin1String("name")) {
QString author = readElementText();
- video->setAuthor(author);
+ video->setChannelTitle(author);
} else if (name() == QLatin1String("userId")) {
QString userId = readElementText();
- video->setUserId(userId);
+ video->setChannelId(userId);
} else skipCurrentElement();
} else if (name() == QLatin1String("published")) {
video->setPublished(QDateTime::fromString(readElementText(), Qt::ISODate));
#ifndef YTFEEDREADER_H
#define YTFEEDREADER_H
-#include <QtXml>
+#include <QtCore>
class Video;
$END_LICENSE */
#include "ytsearch.h"
-#include "ytfeedreader.h"
#include "constants.h"
#include "networkaccess.h"
#include "searchparams.h"
#include "video.h"
-#include "ytuser.h"
+#include "ytchannel.h"
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include "yt3listparser.h"
+#include "datautils.h"
+#else
+#include "ytfeedreader.h"
+#endif
namespace The {
NetworkAccess* http();
QHash<QString, QAction*>* globalActions();
}
+namespace {
+
+QDateTime RFC3339fromString(const QString &s) {
+ return QDateTime::fromString(s, "yyyy-MM-ddThh:mm:ssZ");
+}
+
+QString RFC3339toString(const QDateTime &dt) {
+ return dt.toString("yyyy-MM-ddThh:mm:ssZ");
+}
+
+}
+
YTSearch::YTSearch(SearchParams *searchParams, QObject *parent) :
- VideoSource(parent),
+ PaginatedVideoSource(parent),
searchParams(searchParams) {
searchParams->setParent(this);
}
-void YTSearch::loadVideos(int max, int skip) {
+#ifdef APP_YT3
+
+void YTSearch::loadVideos(int max, int startIndex) {
aborted = false;
- QUrl url("http://gdata.youtube.com/feeds/api/videos/");
+ QUrl url = YT3::instance().method("search");
+
#if QT_VERSION >= 0x050000
-{
- QUrl &u = url;
- QUrlQuery url;
+ {
+ QUrl &u = url;
+ QUrlQuery url;
#endif
- url.addQueryItem("v", "2");
- url.addQueryItem("max-results", QString::number(max));
- url.addQueryItem("start-index", QString::number(skip));
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("type", "video");
- if (!searchParams->keywords().isEmpty()) {
- if (searchParams->keywords().startsWith("http://") ||
- searchParams->keywords().startsWith("https://")) {
- url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
- } else url.addQueryItem("q", searchParams->keywords());
- }
+ url.addQueryItem("maxResults", QString::number(max));
- if (!searchParams->author().isEmpty())
- url.addQueryItem("author", searchParams->author());
-
- switch (searchParams->sortBy()) {
- case SearchParams::SortByNewest:
- url.addQueryItem("orderby", "published");
- break;
- case SearchParams::SortByViewCount:
- url.addQueryItem("orderby", "viewCount");
- break;
- case SearchParams::SortByRating:
- url.addQueryItem("orderby", "rating");
- break;
- }
+ if (startIndex > 1) {
+ if (maybeReloadToken(max, startIndex)) return;
+ url.addQueryItem("pageToken", nextPageToken);
+ }
- switch (searchParams->duration()) {
- case SearchParams::DurationShort:
- url.addQueryItem("duration", "short");
- break;
- case SearchParams::DurationMedium:
- url.addQueryItem("duration", "medium");
- break;
- case SearchParams::DurationLong:
- url.addQueryItem("duration", "long");
- break;
- }
+ // TODO interesting params
+ // url.addQueryItem("videoSyndicated", "true");
+ // url.addQueryItem("regionCode", "IT");
+ // url.addQueryItem("videoType", "movie");
- switch (searchParams->time()) {
- case SearchParams::TimeToday:
- url.addQueryItem("time", "today");
- break;
- case SearchParams::TimeWeek:
- url.addQueryItem("time", "this_week");
- break;
- case SearchParams::TimeMonth:
- url.addQueryItem("time", "this_month");
- break;
- }
+ if (!searchParams->keywords().isEmpty()) {
+ if (searchParams->keywords().startsWith("http://") ||
+ searchParams->keywords().startsWith("https://")) {
+ url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
+ } else url.addQueryItem("q", searchParams->keywords());
+ }
- switch (searchParams->quality()) {
- case SearchParams::QualityHD:
- url.addQueryItem("hd", "true");
- break;
- }
+ if (!searchParams->channelId().isEmpty())
+ url.addQueryItem("channelId", searchParams->channelId());
+
+ switch (searchParams->sortBy()) {
+ case SearchParams::SortByNewest:
+ url.addQueryItem("order", "date");
+ break;
+ case SearchParams::SortByViewCount:
+ url.addQueryItem("order", "viewCount");
+ break;
+ case SearchParams::SortByRating:
+ url.addQueryItem("order", "rating");
+ break;
+ }
+
+ switch (searchParams->duration()) {
+ case SearchParams::DurationShort:
+ url.addQueryItem("videoDuration", "short");
+ break;
+ case SearchParams::DurationMedium:
+ url.addQueryItem("videoDuration", "medium");
+ break;
+ case SearchParams::DurationLong:
+ url.addQueryItem("videoDuration", "long");
+ break;
+ }
+
+ switch (searchParams->time()) {
+ case SearchParams::TimeToday:
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60*60*24)));
+ break;
+ case SearchParams::TimeWeek:
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60*60*24*7)));
+ break;
+ case SearchParams::TimeMonth:
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::currentDateTimeUtc().addSecs(-60*60*24*30)));
+ break;
+ }
+
+ if (searchParams->publishedAfter()) {
+ url.addQueryItem("publishedAfter", RFC3339toString(QDateTime::fromTime_t(searchParams->publishedAfter()).toUTC()));
+ }
+
+ switch (searchParams->quality()) {
+ case SearchParams::QualityHD:
+ url.addQueryItem("videoDefinition", "high");
+ break;
+ }
#if QT_VERSION >= 0x050000
u.setQuery(url);
}
#endif
+
+ lastUrl = url;
+
+ // qWarning() << "YT3 search" << url.toString();
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
-void YTSearch::abort() {
- aborted = true;
-}
+void YTSearch::parseResults(QByteArray data) {
+ if (aborted) return;
-const QStringList & YTSearch::getSuggestions() {
- return suggestions;
+ YT3ListParser parser(data);
+ QList<Video*> videos = parser.getVideos();
+ suggestions = parser.getSuggestions();
+
+ bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
+ if (tryingWithNewToken) return;
+
+ if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
+ if (!videos.isEmpty()) {
+ name = videos.first()->channelTitle();
+ }
+ emit nameChanged(name);
+ }
+
+ if (asyncDetails) {
+ emit gotVideos(videos);
+ emit finished(videos.size());
+ }
+ loadVideoDetails(videos);
}
-QString YTSearch::getName() {
- if (!name.isEmpty()) return name;
- if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
- return QString();
+#else
+
+void YTSearch::loadVideos(int max, int startIndex) {
+ aborted = false;
+
+ QUrl url("http://gdata.youtube.com/feeds/api/videos/");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+
+ url.addQueryItem("v", "2");
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(startIndex));
+
+ if (!searchParams->keywords().isEmpty()) {
+ if (searchParams->keywords().startsWith("http://") ||
+ searchParams->keywords().startsWith("https://")) {
+ url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
+ } else url.addQueryItem("q", searchParams->keywords());
+ }
+
+ if (!searchParams->channelId().isEmpty())
+ url.addQueryItem("author", searchParams->channelId());
+
+ switch (searchParams->sortBy()) {
+ case SearchParams::SortByNewest:
+ url.addQueryItem("orderby", "published");
+ break;
+ case SearchParams::SortByViewCount:
+ url.addQueryItem("orderby", "viewCount");
+ break;
+ case SearchParams::SortByRating:
+ url.addQueryItem("orderby", "rating");
+ break;
+ }
+
+ switch (searchParams->duration()) {
+ case SearchParams::DurationShort:
+ url.addQueryItem("duration", "short");
+ break;
+ case SearchParams::DurationMedium:
+ url.addQueryItem("duration", "medium");
+ break;
+ case SearchParams::DurationLong:
+ url.addQueryItem("duration", "long");
+ break;
+ }
+
+ switch (searchParams->time()) {
+ case SearchParams::TimeToday:
+ url.addQueryItem("time", "today");
+ break;
+ case SearchParams::TimeWeek:
+ url.addQueryItem("time", "this_week");
+ break;
+ case SearchParams::TimeMonth:
+ url.addQueryItem("time", "this_month");
+ break;
+ }
+
+ switch (searchParams->quality()) {
+ case SearchParams::QualityHD:
+ url.addQueryItem("hd", "true");
+ break;
+ }
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
void YTSearch::parseResults(QByteArray data) {
QList<Video*> videos = reader.getVideos();
suggestions = reader.getSuggestions();
- if (name.isEmpty() && !searchParams->author().isEmpty()) {
- if (videos.isEmpty()) name = searchParams->author();
+ if (name.isEmpty() && !searchParams->channelId().isEmpty()) {
+ if (videos.isEmpty()) name = searchParams->channelId();
else {
- name = videos.first()->author();
+ name = videos.first()->channelTitle();
// also grab the userId
- userId = videos.first()->userId();
+ userId = videos.first()->channelId();
}
emit nameChanged(name);
}
emit finished(videos.size());
}
+#endif
+
+void YTSearch::abort() {
+ aborted = true;
+}
+
+const QStringList & YTSearch::getSuggestions() {
+ return suggestions;
+}
+
+QString YTSearch::getName() {
+ if (!name.isEmpty()) return name;
+ if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
+ return QString();
+}
+
void YTSearch::requestError(QNetworkReply *reply) {
+ qWarning() << reply->errorString();
emit error(reply->errorString());
}
QList<QAction*> YTSearch::getActions() {
QList<QAction*> channelActions;
- if (searchParams->author().isEmpty())
+ if (searchParams->channelId().isEmpty())
return channelActions;
channelActions << The::globalActions()->value("subscribe-channel");
return channelActions;
#define YTSEARCH_H
#include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
class SearchParams;
class Video;
-class YTSearch : public VideoSource {
+class YTSearch : public PaginatedVideoSource {
Q_OBJECT
public:
YTSearch(SearchParams *params, QObject *parent = 0);
- void loadVideos(int max, int skip);
- virtual void abort();
- virtual const QStringList & getSuggestions();
- static QString videoIdFromUrl(QString url);
+ void loadVideos(int max, int startIndex);
+ void abort();
+ const QStringList & getSuggestions();
QString getName();
+ QList<QAction*> getActions();
SearchParams* getSearchParams() const { return searchParams; }
+ static QString videoIdFromUrl(QString url);
bool operator==(const YTSearch &other) const {
return searchParams == other.getSearchParams();
}
- QList<QAction*> getActions();
-
private slots:
void parseResults(QByteArray data);
void requestError(QNetworkReply *reply);
bool aborted;
QStringList suggestions;
QString name;
-
- QString userId;
};
#endif // YTSEARCH_H
$END_LICENSE */
#include "ytsinglevideosource.h"
-#include <QtXml>
#include "networkaccess.h"
#include "video.h"
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include "yt3listparser.h"
+#else
#include "ytfeedreader.h"
+#endif
namespace The {
NetworkAccess* http();
}
-YTSingleVideoSource::YTSingleVideoSource(QObject *parent) : VideoSource(parent) {
- skip = 0;
- max = 0;
+YTSingleVideoSource::YTSingleVideoSource(QObject *parent) : PaginatedVideoSource(parent),
+ video(0),
+ startIndex(0),
+ max(0) { }
+
+#ifdef APP_YT3
+
+void YTSingleVideoSource::loadVideos(int max, int startIndex) {
+ aborted = false;
+ this->startIndex = startIndex;
+ this->max = max;
+
+ QUrl url;
+
+ if (startIndex == 1) {
+
+ if (video) {
+ QList<Video*> videos;
+ videos << video->clone();
+ if (name.isEmpty()) {
+ name = videos.first()->title();
+ qDebug() << "Emitting name changed" << name;
+ emit nameChanged(name);
+ }
+ emit gotVideos(videos);
+ loadVideos(max - 1, 2);
+ return;
+ }
+
+ url = YT3::instance().method("videos");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("id", videoId);
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ } else {
+ url = YT3::instance().method("search");
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+ url.addQueryItem("part", "snippet");
+ url.addQueryItem("type", "video");
+ url.addQueryItem("relatedToVideoId", videoId);
+ url.addQueryItem("maxResults", QString::number(max));
+ if (startIndex > 2) {
+ if (maybeReloadToken(max, startIndex)) return;
+ url.addQueryItem("pageToken", nextPageToken);
+ }
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ }
+
+ lastUrl = url;
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTSingleVideoSource::parseResults(QByteArray data) {
+ if (aborted) return;
+
+ YT3ListParser parser(data);
+ QList<Video*> videos = parser.getVideos();
+
+ bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
+ if (tryingWithNewToken) return;
+
+ if (asyncDetails) {
+ emit gotVideos(videos);
+ if (startIndex == 2) emit finished(videos.size() + 1);
+ else emit finished(videos.size());
+ }
+ loadVideoDetails(videos);
}
-void YTSingleVideoSource::loadVideos(int max, int skip) {
+#else
+
+void YTSingleVideoSource::loadVideos(int max, int startIndex) {
aborted = false;
- this->skip = skip;
+ this->startIndex = startIndex;
this->max = max;
QString s;
- if (skip == 1) s = "http://gdata.youtube.com/feeds/api/videos/" + videoId;
+ if (startIndex == 1) s = "http://gdata.youtube.com/feeds/api/videos/" + videoId;
else s = QString("http://gdata.youtube.com/feeds/api/videos/%1/related").arg(videoId);
QUrl url(s);
#if QT_VERSION >= 0x050000
-{
- QUrl &u = url;
- QUrlQuery url;
+ {
+ QUrl &u = url;
+ QUrlQuery url;
#endif
- url.addQueryItem("v", "2");
+ url.addQueryItem("v", "2");
- if (skip != 1) {
- url.addQueryItem("max-results", QString::number(max));
- url.addQueryItem("start-index", QString::number(skip-1));
- }
+ if (startIndex != 1) {
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(startIndex-1));
+ }
#if QT_VERSION >= 0x050000
u.setQuery(url);
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
-void YTSingleVideoSource::abort() {
- aborted = true;
-}
-
-const QStringList & YTSingleVideoSource::getSuggestions() {
- QStringList *l = new QStringList();
- return *l;
-}
-
-QString YTSingleVideoSource::getName() {
- return name;
-}
-
void YTSingleVideoSource::parse(QByteArray data) {
if (aborted) return;
YTFeedReader reader(data);
QList<Video*> videos = reader.getVideos();
- if (name.isEmpty() && !videos.isEmpty() && skip == 1) {
+ if (name.isEmpty() && !videos.isEmpty() && startIndex == 1) {
name = videos.first()->title();
emit nameChanged(name);
}
emit gotVideos(videos);
- if (skip == 1) loadVideos(max - 1, 2);
- else if (skip == 2) emit finished(videos.size() + 1);
+ if (startIndex == 1) loadVideos(max - 1, 2);
+ else if (startIndex == 2) emit finished(videos.size() + 1);
else emit finished(videos.size());
}
+#endif
+
+void YTSingleVideoSource::abort() {
+ aborted = true;
+}
+
+const QStringList & YTSingleVideoSource::getSuggestions() {
+ static const QStringList *l = new QStringList();
+ return *l;
+}
+
+QString YTSingleVideoSource::getName() {
+ return name;
+}
+
+void YTSingleVideoSource::setVideo(Video *video) {
+ this->video = video;
+ videoId = video->id();
+}
+
void YTSingleVideoSource::requestError(QNetworkReply *reply) {
emit error(reply->errorString());
}
#define YTSINGLEVIDEOSOURCE_H
#include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
-class YTSingleVideoSource : public VideoSource {
+class YTSingleVideoSource : public PaginatedVideoSource {
Q_OBJECT
public:
YTSingleVideoSource(QObject *parent = 0);
- void loadVideos(int max, int skip);
+ void loadVideos(int max, int startIndex);
void abort();
const QStringList & getSuggestions();
QString getName();
void setVideoId(QString videoId) { this->videoId = videoId; }
+ void setVideo(Video *video);
private slots:
- void parse(QByteArray data);
+ void parseResults(QByteArray data);
void requestError(QNetworkReply *reply);
private:
+ Video *video;
QString videoId;
bool aborted;
- int skip;
+ int startIndex;
int max;
QString name;
};
$END_LICENSE */
#include "ytstandardfeed.h"
-#include <QtXml>
#include "networkaccess.h"
#include "video.h"
+
+#ifdef APP_YT3
+#include "yt3.h"
+#include "yt3listparser.h"
+#else
#include "ytfeedreader.h"
+#endif
namespace The {
NetworkAccess* http();
}
YTStandardFeed::YTStandardFeed(QObject *parent)
- : VideoSource(parent),
+ : PaginatedVideoSource(parent),
aborted(false) { }
-void YTStandardFeed::loadVideos(int max, int skip) {
+#ifdef APP_YT3
+
+void YTStandardFeed::loadVideos(int max, int startIndex) {
+ aborted = false;
+
+ QUrl url = YT3::instance().method("videos");
+
+#if QT_VERSION >= 0x050000
+ {
+ QUrl &u = url;
+ QUrlQuery url;
+#endif
+
+ if (startIndex > 1) {
+ if (maybeReloadToken(max, startIndex)) return;
+ url.addQueryItem("pageToken", nextPageToken);
+ }
+
+ url.addQueryItem("part", "snippet,contentDetails,statistics");
+ url.addQueryItem("chart", "mostPopular");
+
+ if (!category.isEmpty())
+ url.addQueryItem("videoCategoryId", category);
+
+ if (!regionId.isEmpty())
+ url.addQueryItem("regionCode", regionId);
+
+ url.addQueryItem("maxResults", QString::number(max));
+
+#if QT_VERSION >= 0x050000
+ u.setQuery(url);
+ }
+#endif
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTStandardFeed::parseResults(QByteArray data) {
+ if (aborted) return;
+
+ YT3ListParser parser(data);
+ QList<Video*> videos = parser.getVideos();
+
+ bool tryingWithNewToken = setPageToken(parser.getNextPageToken());
+ if (tryingWithNewToken) return;
+
+ if (reloadingToken) {
+ reloadingToken = false;
+ loadVideos(currentMax, currentStartIndex);
+ currentMax = currentStartIndex = 0;
+ return;
+ }
+
+ emit gotVideos(videos);
+ emit finished(videos.size());
+}
+
+#else
+
+void YTStandardFeed::loadVideos(int max, int startIndex) {
aborted = false;
QString s = "http://gdata.youtube.com/feeds/api/standardfeeds/";
QUrl url(s);
#if QT_VERSION >= 0x050000
-{
- QUrl &u = url;
- QUrlQuery url;
+ {
+ QUrl &u = url;
+ QUrlQuery url;
#endif
- url.addQueryItem("v", "2");
+ url.addQueryItem("v", "2");
- if (feedId != "most_shared" && feedId != "on_the_web") {
- QString t = time;
- if (t.isEmpty()) t = "today";
- url.addQueryItem("time", t);
- }
+ if (feedId != "most_shared" && feedId != "on_the_web") {
+ QString t = time;
+ if (t.isEmpty()) t = "today";
+ url.addQueryItem("time", t);
+ }
- url.addQueryItem("max-results", QString::number(max));
- url.addQueryItem("start-index", QString::number(skip));
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(startIndex));
#if QT_VERSION >= 0x050000
u.setQuery(url);
connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
}
-void YTStandardFeed::abort() {
- aborted = true;
-}
-
-const QStringList & YTStandardFeed::getSuggestions() {
- QStringList *l = new QStringList();
- return *l;
-}
-
-void YTStandardFeed::parse(QByteArray data) {
+void YTStandardFeed::parseResults(QByteArray data) {
if (aborted) return;
YTFeedReader reader(data);
emit finished(videos.size());
}
+#endif
+
+void YTStandardFeed::abort() {
+ aborted = true;
+}
+
+const QStringList & YTStandardFeed::getSuggestions() {
+ QStringList *l = new QStringList();
+ return *l;
+}
+
void YTStandardFeed::requestError(QNetworkReply *reply) {
emit error(reply->errorString());
}
#define YTSTANDARDFEED_H
#include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
-class YTStandardFeed : public VideoSource {
+class YTStandardFeed : public PaginatedVideoSource {
Q_OBJECT
QString getTime() { return time; }
void setTime(QString time) { this->time = time; }
- void loadVideos(int max, int skip);
+ void loadVideos(int max, int startIndex);
void abort();
const QStringList & getSuggestions();
QString getName() { return label; }
private slots:
- void parse(QByteArray data);
+ void parseResults(QByteArray data);
void requestError(QNetworkReply *reply);
private:
$END_LICENSE */
#include "ytsuggester.h"
-#include <QtXml>
#include "networkaccess.h"
-#define GSUGGEST_URL "http://suggestqueries.google.com/complete/search?ds=yt&output=toolbar&hl=%1&q=%2"
-
namespace The {
- NetworkAccess* http();
+NetworkAccess* http();
}
YTSuggester::YTSuggester(QObject *parent) : Suggester(parent) {
locale = "en-US";
}
- QString url = QString(GSUGGEST_URL).arg(locale, query);
+ QString url =
+ QString("https://suggestqueries.google.com/complete/search?ds=yt&output=toolbar&hl=%1&q=%2")
+ .arg(locale, query);
QObject *reply = The::http()->get(url);
connect(reply, SIGNAL(data(QByteArray)), SLOT(handleNetworkData(QByteArray)));
+++ /dev/null
-/* $BEGIN_LICENSE
-
-This file is part of Minitube.
-Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
-
-Minitube is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Minitube is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Minitube. If not, see <http://www.gnu.org/licenses/>.
-
-$END_LICENSE */
-
-#include "ytuser.h"
-#include "networkaccess.h"
-#include "database.h"
-#include <QtSql>
-
-namespace The {
-NetworkAccess* http();
-}
-
-YTUser::YTUser(QString userId, QObject *parent) : QObject(parent),
- id(0),
- userId(userId),
- loadingThumbnail(false),
- notifyCount(0),
- checked(0),
- watched(0),
- loaded(0),
- loading(false) { }
-
-QHash<QString, YTUser*> YTUser::cache;
-
-YTUser* YTUser::forId(QString userId) {
- if (userId.isEmpty()) return 0;
-
- if (cache.contains(userId))
- return cache.value(userId);
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("select id,name,description,thumb_url,notify_count,watched,checked,loaded "
- "from subscriptions where user_id=?");
- query.bindValue(0, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- YTUser* user = 0;
- if (query.next()) {
- user = new YTUser(userId);
- user->id = query.value(0).toInt();
- user->displayName = query.value(1).toString();
- user->description = query.value(2).toString();
- user->thumbnailUrl = query.value(3).toString();
- user->notifyCount = query.value(4).toInt();
- user->watched = query.value(5).toUInt();
- user->checked = query.value(6).toUInt();
- user->loaded = query.value(7).toUInt();
- user->thumbnail = QPixmap(user->getThumbnailLocation());
- user->maybeLoadfromAPI();
- cache.insert(userId, user);
- }
-
- return user;
-}
-
-void YTUser::maybeLoadfromAPI() {
- if (loading) return;
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
- static const int refreshInterval = 60 * 60 * 24 * 10;
- if (loaded > now - refreshInterval) return;
-
- loading = true;
-
- QUrl url("http://gdata.youtube.com/feeds/api/users/" + userId);
-#if QT_VERSION >= 0x050000
- {
- QUrl &u = url;
- QUrlQuery url;
-#endif
- url.addQueryItem("v", "2");
-#if QT_VERSION >= 0x050000
- u.setQuery(url);
- }
-#endif
- QObject *reply = The::http()->get(url);
- connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResponse(QByteArray)));
- connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
-}
-
-void YTUser::parseResponse(QByteArray bytes) {
- QXmlStreamReader xml(bytes);
- xml.readNextStartElement();
- if (xml.name() == QLatin1String("entry"))
- while(xml.readNextStartElement()) {
- const QStringRef n = xml.name();
- if (n == QLatin1String("summary"))
- description = xml.readElementText().simplified();
- else if (n == QLatin1String("title"))
- displayName = xml.readElementText();
- else if (n == QLatin1String("thumbnail")) {
- thumbnailUrl = xml.attributes().value("url").toString();
- xml.skipCurrentElement();
- } else if (n == QLatin1String("username"))
- userName = xml.readElementText();
- else xml.skipCurrentElement();
- }
-
- if (xml.hasError()) {
- emit error(xml.errorString());
- qWarning() << xml.errorString();
- }
-
- emit infoLoaded();
- storeInfo();
- loading = false;
-}
-
-void YTUser::loadThumbnail() {
- if (loadingThumbnail) return;
- if (thumbnailUrl.isEmpty()) return;
- loadingThumbnail = true;
-
-#ifdef Q_OS_WIN
- thumbnailUrl.replace(QLatin1String("https://"), QLatin1String("http://"));
-#endif
-
- QUrl url(thumbnailUrl);
- QObject *reply = The::http()->get(url);
- connect(reply, SIGNAL(data(QByteArray)), SLOT(storeThumbnail(QByteArray)));
- connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
-}
-
-const QString & YTUser::getThumbnailDir() {
- static const QString thumbDir =
- #if QT_VERSION >= 0x050000
- QStandardPaths::writableLocation(QStandardPaths::DataLocation)
- #else
- QDesktopServices::storageLocation(QDesktopServices::DataLocation)
- #endif
- + "/channels/";
- return thumbDir;
-}
-
-QString YTUser::getThumbnailLocation() {
- return getThumbnailDir() + userId;
-}
-
-void YTUser::unsubscribe() {
- YTUser::unsubscribe(userId);
-}
-
-void YTUser::storeThumbnail(QByteArray bytes) {
- thumbnail.loadFromData(bytes);
- static const int maxWidth = 88;
-
- QDir dir;
- dir.mkpath(getThumbnailDir());
-
- if (thumbnail.width() > maxWidth) {
- thumbnail = thumbnail.scaledToWidth(maxWidth, Qt::SmoothTransformation);
- thumbnail.save(getThumbnailLocation(), "JPG");
- } else {
- QFile file(getThumbnailLocation());
- if (!file.open(QIODevice::WriteOnly))
- qWarning() << "Error opening file for writing" << file.fileName();
- QDataStream stream(&file);
- stream.writeRawData(bytes.constData(), bytes.size());
- }
-
- emit thumbnailLoaded();
- loadingThumbnail = false;
-}
-
-void YTUser::requestError(QNetworkReply *reply) {
- emit error(reply->errorString());
- qWarning() << reply->errorString();
- loading = false;
- loadingThumbnail = false;
-}
-
-void YTUser::storeInfo() {
- if (userId.isEmpty()) return;
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set "
- "user_name=?, name=?, description=?, thumb_url=?, loaded=? "
- "where user_id=?");
- qDebug() << userName;
- query.bindValue(0, userName);
- query.bindValue(1, displayName);
- query.bindValue(2, description);
- query.bindValue(3, thumbnailUrl);
- query.bindValue(4, QDateTime::currentDateTime().toTime_t());
- query.bindValue(5, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- loadThumbnail();
-}
-
-void YTUser::subscribe(QString userId) {
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("insert into subscriptions "
- "(user_id,added,watched,checked,views,notify_count)"
- " values (?,?,?,0,0,0)");
- query.bindValue(0, userId);
- query.bindValue(1, now);
- query.bindValue(2, now);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- // This will call maybeLoadFromApi
- YTUser::forId(userId);
-}
-
-void YTUser::unsubscribe(QString userId) {
- if (userId.isEmpty()) return;
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("delete from subscriptions where user_id=?");
- query.bindValue(0, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- query = QSqlQuery(db);
- query.prepare("delete from subscriptions_videos where user_id=?");
- query.bindValue(0, userId);
- success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-
- YTUser *user = cache.take(userId);
- if (user) user->deleteLater();
-}
-
-bool YTUser::isSubscribed(QString userId) {
- if (!Database::exists()) return false;
- if (userId.isEmpty()) return false;
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("select count(*) from subscriptions where user_id=?");
- query.bindValue(0, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
- if (query.next())
- return query.value(0).toInt() > 0;
- return false;
-}
-
-void YTUser::updateChecked() {
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
- checked = now;
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set checked=? where user_id=?");
- query.bindValue(0, QDateTime::currentDateTime().toTime_t());
- query.bindValue(1, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-}
-
-void YTUser::updateWatched() {
- if (userId.isEmpty()) return;
-
- uint now = QDateTime::currentDateTime().toTime_t();
- watched = now;
- notifyCount = 0;
- emit notifyCountChanged();
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set watched=?, notify_count=0, views=views+1 where user_id=?");
- query.bindValue(0, now);
- query.bindValue(1, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-}
-
-void YTUser::storeNotifyCount(int count) {
- if (notifyCount != count)
- emit notifyCountChanged();
- notifyCount = count;
-
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("update subscriptions set notify_count=? where user_id=?");
- query.bindValue(0, count);
- query.bindValue(1, userId);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
-}
-
-bool YTUser::updateNotifyCount() {
- QSqlDatabase db = Database::instance().getConnection();
- QSqlQuery query(db);
- query.prepare("select count(*) from subscriptions_videos "
- "where channel_id=? and added>? and published>? and watched=0");
- query.bindValue(0, id);
- query.bindValue(1, watched);
- query.bindValue(2, watched);
- bool success = query.exec();
- if (!success) qWarning() << query.lastQuery() << query.lastError().text();
- if (!query.next()) {
- qWarning() << __PRETTY_FUNCTION__ << "Count failed";
- return false;
- }
- int count = query.value(0).toInt();
- storeNotifyCount(count);
- return count != notifyCount;
-}
+++ /dev/null
-/* $BEGIN_LICENSE
-
-This file is part of Minitube.
-Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
-
-Minitube is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Minitube is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Minitube. If not, see <http://www.gnu.org/licenses/>.
-
-$END_LICENSE */
-
-#ifndef YTUSER_H
-#define YTUSER_H
-
-#include <QtGui>
-#if QT_VERSION >= 0x050000
-#include <QtWidgets>
-#endif
-#include <QtNetwork>
-
-class YTUser : public QObject {
-
- Q_OBJECT
-
-public:
- static YTUser* forId(QString userId);
- static void subscribe(QString userId);
- static void unsubscribe(QString userId);
- static bool isSubscribed(QString userId);
-
- int getId() { return id; }
- void setId(int id) { this->id = id; }
-
- uint getChecked() { return checked; }
- void updateChecked();
-
- uint getWatched() const { return watched; }
- void setWatched(uint watched) { this->watched = watched; }
-
- int getNotifyCount() const { return notifyCount; }
- void setNotifyCount(int count) { notifyCount = count; }
- void storeNotifyCount(int count);
- bool updateNotifyCount();
-
- QString getUserId() const { return userId; }
- QString getUserName() const { return userName; }
- QString getDisplayName() const { return displayName; }
- QString getDescription() const { return description; }
- QString getCountryCode() const { return countryCode; }
-
- void loadThumbnail();
- const QString & getThumbnailDir();
- QString getThumbnailLocation();
- const QPixmap & getThumbnail() { return thumbnail; }
-
- static QList<YTUser*> getCachedUsers() { return cache.values(); }
-
-public slots:
- void updateWatched();
- void unsubscribe();
-
-signals:
- void infoLoaded();
- void thumbnailLoaded();
- void error(QString message);
- void notifyCountChanged();
-
-private slots:
- void parseResponse(QByteArray bytes);
- void requestError(QNetworkReply *reply);
- void storeThumbnail(QByteArray bytes);
-
-private:
- YTUser(QString userId, QObject *parent = 0);
- void maybeLoadfromAPI();
- void storeInfo();
-
- static QHash<QString, YTUser*> cache;
-
- int id;
- QString userId;
- QString userName;
- QString displayName;
- QString description;
- QString countryCode;
-
- QString thumbnailUrl;
- QPixmap thumbnail;
- bool loadingThumbnail;
-
- int notifyCount;
- uint checked;
- uint watched;
- uint loaded;
- bool loading;
-};
-
-// This is required in order to use QPointer<YTUser> as a QVariant
-typedef QPointer<YTUser> YTUserPointer;
-Q_DECLARE_METATYPE(YTUserPointer)
-
-#endif // YTUSER_H