]> git.sur5r.net Git - minitube/commitdiff
Imported Upstream version 2.4 upstream/2.4
authorJakob Haufe <sur5r@sur5r.net>
Sun, 7 Jun 2015 01:10:01 +0000 (03:10 +0200)
committerJakob Haufe <sur5r@sur5r.net>
Sun, 7 Jun 2015 01:10:01 +0000 (03:10 +0200)
84 files changed:
.gitignore
CHANGES
INSTALL [deleted file]
README.md [new file with mode: 0644]
locale/ca.ts
locale/da.ts
locale/el.ts
locale/fi.ts
locale/he_IL.ts
locale/hu.ts
locale/ja_JP.ts
locale/ko_KR.ts [new file with mode: 0644]
locale/ky.ts
locale/pl_PL.ts
locale/pt.ts
locale/pt_BR.ts
locale/sv_SE.ts
locale/th.ts [new file with mode: 0644]
minitube.pro
src/aboutview.cpp
src/aggregatevideosource.cpp
src/aggregatevideosource.h
src/channelaggregator.cpp
src/channelaggregator.h
src/channelitemdelegate.cpp
src/channelmodel.cpp
src/channelmodel.h
src/channelsitemdelegate.cpp [new file with mode: 0644]
src/channelsitemdelegate.h [new file with mode: 0644]
src/channelsmodel.cpp [new file with mode: 0644]
src/channelsmodel.h [new file with mode: 0644]
src/channelsuggest.cpp
src/channelsview.cpp [new file with mode: 0644]
src/channelsview.h [new file with mode: 0644]
src/channelview.cpp
src/channelwidget.cpp [new file with mode: 0644]
src/channelwidget.h [new file with mode: 0644]
src/database.cpp
src/database.h
src/datautils.cpp
src/datautils.h
src/diskcache.cpp
src/diskcache.h
src/jsfunctions.cpp
src/jsfunctions.h
src/main.cpp
src/mainwindow.cpp
src/mediaview.cpp
src/networkaccess.cpp
src/paginatedvideosource.cpp [new file with mode: 0644]
src/paginatedvideosource.h [new file with mode: 0644]
src/painterutils.cpp
src/playlistitemdelegate.cpp
src/playlistmodel.cpp
src/playlistmodel.h
src/playlistview.cpp
src/searchparams.cpp
src/searchparams.h
src/searchview.cpp
src/searchview.h
src/snapshotsettings.cpp
src/standardfeedsview.cpp
src/suggester.h
src/video.cpp
src/video.h
src/videosource.h
src/yt3.cpp [new file with mode: 0644]
src/yt3.h [new file with mode: 0644]
src/yt3listparser.cpp [new file with mode: 0644]
src/yt3listparser.h [new file with mode: 0644]
src/ytcategories.cpp
src/ytchannel.cpp [new file with mode: 0644]
src/ytchannel.h [new file with mode: 0644]
src/ytfeedreader.cpp
src/ytfeedreader.h
src/ytsearch.cpp
src/ytsearch.h
src/ytsinglevideosource.cpp
src/ytsinglevideosource.h
src/ytstandardfeed.cpp
src/ytstandardfeed.h
src/ytsuggester.cpp
src/ytuser.cpp [deleted file]
src/ytuser.h [deleted file]

index c645b5d9713d1506f38aa9a1f58909254d3aa01c..bb53302cd245aa5acde739e91f9da27e1d5b69bc 100644 (file)
@@ -10,3 +10,5 @@ local/
 .tx
 android
 qtc_packaging
+debian
+
diff --git a/CHANGES b/CHANGES
index e135e4c5ec5665191c5bf905b52c3707a12ea5a9..49ec395f98bebcdfbfceb11ee3e908f8ee1880d6 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,15 @@
+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
@@ -5,6 +17,7 @@
 - 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
diff --git a/INSTALL b/INSTALL
deleted file mode 100644 (file)
index 169bf90..0000000
--- a/INSTALL
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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/>.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..0d8ac19
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# 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/>.
index 859749b97a1d2434b64f5a8c8aa3ef844ec4ce7a..93e7e2fe541e285ec61f31b6943afab6061800b4 100644 (file)
         <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>Pren &amp;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>
index 7d2aa40d95452763e1cd1487ac48b9c72c241141..5368e1a735bcfa6d47f8feb04fa4383dfca5e71d 100644 (file)
         <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 &amp;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>&amp;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"/>
@@ -1340,12 +1340,12 @@ Kopiér &amp;URL&apos;en til videostrømmen</translation>
     <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>
index 5c4e94a8bb48f802cb9b6aa7fb73329e884e9222..2e0a743e7e680c01e6d7cdd49544c3bb3890bab6 100644 (file)
         <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>Λήψη &amp;Στιγμιότυπου</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="493"/>
         <source>&amp;Subscribe to Channel</source>
-        <translation>&amp;ΣÏ\85δÏ\81Î¿Î¼ή στο Κανάλι</translation>
+        <translation>&amp;Î\95γγÏ\81αÏ\86ή στο Κανάλι</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="501"/>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>&amp;Λατρεύετε το %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>
index 5d2e2e71a4d9a284ecb04b2af2078ce13467842b..bac158b10a8c3b272c0bb84ee5d097174ef724dd 100644 (file)
         <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>Ota &amp;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>
index 87a1f00d085e4020e80df59a65a734f66831edc7..361c5cc9411568cf82b1a57007b055827eb3ade2 100644 (file)
@@ -65,7 +65,7 @@
     <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>קח צילום מסך</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="493"/>
         <source>&amp;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>&amp;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 &amp;Browser...</source>
-        <translation type="unfinished"/>
+        <translation>פתח בדפדפן</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>&amp;אוהב את %1? דרג אותו!</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="605"/>
     <message>
         <location filename="src/mainwindow.cpp" line="397"/>
         <source>Make a &amp;Donation</source>
-        <translation>ה&amp;גשת תרומה</translation>
+        <translation>&amp;תרמו</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>&amp;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>
index 070f7c07b7c1bfee897d30adc242096efd938ed2..15c35488b723b2572b3b22493c65e5385c0bd551 100644 (file)
@@ -15,7 +15,7 @@
     <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"/>
@@ -78,7 +78,7 @@
     <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>&amp;Snapshot készítése</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="493"/>
         <source>&amp;Subscribe to Channel</source>
-        <translation type="unfinished"/>
+        <translation>&amp;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 &amp;Browser...</source>
-        <translation type="unfinished"/>
+        <translation>Megtekintés a következővel:</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>&amp;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>
index 20107f730dad30adf060fb271a402aefe3382bca..ae684925b76b0f45d9b0d8c1f856a24a13ad9224 100644 (file)
@@ -15,7 +15,7 @@
     <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 &lt;a href=&apos;%1&apos;&gt;donate&lt;/a&gt; to support the continued development of %2.</source>
-        <translation type="unfinished"/>
+        <translation>継続的な開発を&lt;a href=&apos;%1&apos;&gt;支援する&lt;/a&gt;には%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"/>
@@ -65,7 +65,7 @@
     <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"/>
@@ -78,7 +78,7 @@
     <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>&amp;Stop</source>
-        <translation>ストップ(&amp;S)</translation>
+        <translation>再生停止(&amp;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&amp;kip</source>
-        <translation>スキップ(&amp;k)</translation>
+        <translation>次へ(&amp;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>&amp;Full Screen</source>
-        <translation>フルスクリーン(&amp;F)</translation>
+        <translation>フルスクリーンモード(&amp;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>&amp;Website</source>
-        <translation>&amp;Webページへ</translation>
+        <translation>ウェブサイト(&amp;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>&amp;About</source>
-        <translation>プログラムについて(&amp;A)</translation>
+        <translation>Minitubeについて(&amp;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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>スナップショットを撮る(&amp;S)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="493"/>
         <source>&amp;Subscribe to Channel</source>
-        <translation type="unfinished"/>
+        <translation>チャンネル購読(&amp;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>&amp;Float on Top</source>
-        <translation type="unfinished"/>
+        <translation>最前面に表示(&amp;F)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="542"/>
         <source>&amp;Stop After This Video</source>
-        <translation type="unfinished"/>
+        <translation>再生終了後に停止(&amp;S)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="549"/>
         <source>&amp;Report an Issue...</source>
-        <translation>問題点を報告(&amp;R)...</translation>
+        <translation>問題点を報告する(&amp;R)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="553"/>
         <source>&amp;Refine Search...</source>
-        <translation type="unfinished"/>
+        <translation>絞り込み検索(&amp;R)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="565"/>
     <message>
         <location filename="src/mainwindow.cpp" line="568"/>
         <source>&amp;Related Videos</source>
-        <translation type="unfinished"/>
+        <translation>関連動画(&amp;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 &amp;Browser...</source>
-        <translation type="unfinished"/>
+        <translation>ブラウザで見る(&amp;B)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>%1を評価(&amp;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>&amp;Compact Mode</source>
-        <translation>コンパクト モード(&amp;C)</translation>
+        <translation>コンパクトモード(&amp;C)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="324"/>
         <source>Open the &amp;YouTube Page</source>
-        <translation>YouTube のページを開く(&amp;Y)</translation>
+        <translation>YouTubeのページを開く(&amp;Y)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="331"/>
         <source>Copy the YouTube &amp;Link</source>
-        <translation>YouTube のリンクをコピー(&amp;L)</translation>
+        <translation>YouTubeのリンクをコピー(&amp;L)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="338"/>
         <source>Copy the Video Stream &amp;URL</source>
-        <translation type="unfinished"/>
+        <translation>動画のURLをコピー(&amp;U)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="345"/>
         <source>Find Video &amp;Parts</source>
-        <translation type="unfinished"/>
+        <translation>他のパートを探す(&amp;P)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="373"/>
         <source>&amp;Clear Recent Searches</source>
-        <translation>æ\9c\80è¿\91ã\81®æ¤\9cç´¢を消去(&amp;C)</translation>
+        <translation>æ¤\9c索履歴を消去(&amp;C)</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="397"/>
     <message>
         <location filename="src/mainwindow.cpp" line="459"/>
         <source>&amp;Manually Start Playing</source>
-        <translation type="unfinished"/>
+        <translation>手動で再生(&amp;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 &lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;,</source>
-        <translation>ようこそ&lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;へ</translation>
+        <translation>ようこそ&lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;へ</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>&quot;Enter&quot;, as in &quot;type&quot;. The whole phrase says: &quot;Enter a keyword to start watching videos&quot;</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"/>
diff --git a/locale/ko_KR.ts b/locale/ko_KR.ts
new file mode 100644 (file)
index 0000000..6fb50fa
--- /dev/null
@@ -0,0 +1,1614 @@
+<?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&apos;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 &lt;a href=&apos;%1&apos;&gt;donate&lt;/a&gt; to support the continued development of %2.</source>
+        <translation>%2의 계속된 개발을 위해  &lt;a href=&apos;%1&apos;&gt;기부&lt;/a&gt;룰 해주요...</translation>
+    </message>
+    <message>
+        <location filename="src/aboutview.cpp" line="67"/>
+        <source>You may want to try my other apps as well:</source>
+        <translation>저희의 다른 프로그램도 사용해 보세요&quot;</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 &lt;a href=&apos;%1&apos;&gt;GNU General Public License&lt;/a&gt;</source>
+        <translation>&lt;a href=&apos;%1&apos;&gt;GNU General Public License&lt;/a&gt;하에 배포 됩니다.</translation>
+    </message>
+    <message>
+        <location filename="src/aboutview.cpp" line="104"/>
+        <source>&amp;Close</source>
+        <translation>닫기(&amp;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>&amp;Email:</source>
+        <translation>Email(&amp;E)</translation>
+    </message>
+    <message>
+        <location filename="local/src/activationdialog.cpp" line="35"/>
+        <source>&amp;Code:</source>
+        <translation>코드(&amp;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>&amp;Stop</source>
+        <translation>정지(&amp;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&amp;kip</source>
+        <translation>건너뛰기(&amp;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>&amp;Pause</source>
+        <translation>일시정지(&amp;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>&amp;Full Screen</source>
+        <translation>전체 화면(&amp;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>&amp;Remove</source>
+        <translation>제거(&amp;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 &amp;Up</source>
+        <translation>위로 이동(&amp;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 &amp;Down</source>
+        <translation>아래로이동(&amp;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>&amp;Quit</source>
+        <translation>종료(&amp;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>&amp;Website</source>
+        <translation>웹사이트(&amp;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>&amp;About</source>
+        <translation>프로그램 정보(&amp;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>&amp;Downloads</source>
+        <translation>다운로드(&amp;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>&amp;Download</source>
+        <translation>다운로드(&amp;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 &amp;Snapshot</source>
+        <translation>스냅샷 찍기(&amp;S)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="493"/>
+        <source>&amp;Subscribe to Channel</source>
+        <translation>채널 구독(&amp;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>&amp;Email</source>
+        <translation>Email(&amp;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>&amp;Close</source>
+        <translation>닫기(&amp;C)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="537"/>
+        <source>&amp;Float on Top</source>
+        <translation>위에 떠있기(&amp;F)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="542"/>
+        <source>&amp;Stop After This Video</source>
+        <translation>이 비디오 재생 후 정지(&amp;S)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="549"/>
+        <source>&amp;Report an Issue...</source>
+        <translation>문제점 보고...(&amp;R)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="553"/>
+        <source>&amp;Refine Search...</source>
+        <translation>검색 재정의(&amp;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>&amp;Related Videos</source>
+        <translation>관련된 비디오...(&amp;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 &amp;Browser...</source>
+        <translation>브라우저에서 열기(&amp;B)...</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="583"/>
+        <source>&amp;Love %1? Rate it!</source>
+        <translation>%1를 좋아하시나요? 평점을 주세요!</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="605"/>
+        <source>&amp;Application</source>
+        <translation>어플리케이션(&amp;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>&amp;Playback</source>
+        <translation>재생(&amp;P)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="633"/>
+        <source>&amp;Playlist</source>
+        <translation>재생 목록(&amp;P)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="642"/>
+        <source>&amp;Video</source>
+        <translation>비디오(&amp;V)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="658"/>
+        <source>&amp;View</source>
+        <translation>보기(&amp;V)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="665"/>
+        <source>&amp;Share</source>
+        <translation>공유(&amp;S)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="679"/>
+        <source>&amp;Help</source>
+        <translation>도움말(&amp;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 &amp;Full Screen</source>
+        <translation>전체 화면 나가기(&amp;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&amp;revious</source>
+        <translation>이전(&amp;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>&amp;Compact Mode</source>
+        <translation>컴팩트 모드(&amp;C)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="324"/>
+        <source>Open the &amp;YouTube Page</source>
+        <translation>YouTube 페이지 열기(&amp;Y)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="331"/>
+        <source>Copy the YouTube &amp;Link</source>
+        <translation>YouTube 링크 복사(&amp;L)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="338"/>
+        <source>Copy the Video Stream &amp;URL</source>
+        <translation>비디오 스트림 URL을 복사(&amp;U)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="345"/>
+        <source>Find Video &amp;Parts</source>
+        <translation>비디오 부분 찾기(&amp;P)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="373"/>
+        <source>&amp;Clear Recent Searches</source>
+        <translation>최근 검색 지우기(&amp;C)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="397"/>
+        <source>Make a &amp;Donation</source>
+        <translation>기부하기(&amp;D)</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="459"/>
+        <source>&amp;Manually Start Playing</source>
+        <translation>수동으로 재생 시작(&amp;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>&amp;Play</source>
+        <translation>재생(&amp;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 &lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;,</source>
+        <translation>&lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;에 오신걸 환영 합니다!</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>&quot;Enter&quot;, as in &quot;type&quot;. The whole phrase says: &quot;Enter a keyword to start watching videos&quot;</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>&amp;Back</source>
+        <translation>뒤로(&amp;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
index d2260db65415badc06cb675d32f315ab243b478a..91eddd137a0b5c8e5e12be24c354df1209d9ce39 100644 (file)
@@ -5,17 +5,17 @@
     <message>
         <location filename="src/aboutview.cpp" line="52"/>
         <source>There&apos;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"/>
@@ -65,7 +65,7 @@
     <message>
         <location filename="src/aboutview.h" line="40"/>
         <source>About</source>
-        <translation>Программа жөнүндө</translation>
+        <translation>Программа тууралуу</translation>
     </message>
     <message>
         <location filename="src/aboutview.h" line="42"/>
@@ -96,7 +96,7 @@
     <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
index c5dd5a6959badc7801246ef7b025c2423782e01f..ea6851c93bf9878ab9646473c432cda2757c1572 100644 (file)
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>Uwie&amp;lbiasz %1? Oceń to!</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="605"/>
index 23fcf513235a2fb3d1bf93663799f610c8c18136..57ccd75fc5e655ba4d37723246a03140b3a93bac 100644 (file)
         <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>Take &amp;Snapshot</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="493"/>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>&amp;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>
index 757c3ce2239219edeabf17cf4bd6de42b2c27e09..e29bc7b5939dab0af5a965740250de2515fd50eb 100644 (file)
     <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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>&amp;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 &amp;Browser...</source>
-        <translation type="unfinished"/>
+        <translation>Abrir no &amp;navegador...</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>&amp;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>
index e6894398fa8538620a832f9a4332ecadf0de09f4..42a307966d77c9546320817471a0961eba76b64f 100644 (file)
@@ -5,7 +5,7 @@
     <message>
         <location filename="src/aboutview.cpp" line="52"/>
         <source>There&apos;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 &amp;Snapshot</source>
-        <translation type="unfinished"/>
+        <translation>Ta &amp;Skärmbild</translation>
     </message>
     <message>
         <location filename="src/mainwindow.cpp" line="493"/>
     <message>
         <location filename="src/mainwindow.cpp" line="583"/>
         <source>&amp;Love %1? Rate it!</source>
-        <translation type="unfinished"/>
+        <translation>&amp;Ä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>
diff --git a/locale/th.ts b/locale/th.ts
new file mode 100644 (file)
index 0000000..d03e968
--- /dev/null
@@ -0,0 +1,1614 @@
+<?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&apos;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 &lt;a href=&apos;%1&apos;&gt;donate&lt;/a&gt; to support the continued development of %2.</source>
+        <translation>กรุณา &lt;a href=&apos;%1&apos;&gt;บริจาค&lt;/a&gt; เพื่อสนับสนุนการพัฒนาโปรแกรม %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 &lt;a href=&apos;%1&apos;&gt;GNU General Public License&lt;/a&gt;</source>
+        <translation>ปล่อยเผยแพร่ภายใต้ &lt;a href=&apos;%1&apos;&gt;GNU General Public License&lt;/a&gt;</translation>
+    </message>
+    <message>
+        <location filename="src/aboutview.cpp" line="104"/>
+        <source>&amp;Close</source>
+        <translation>&amp;ปิด</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>&amp;Email:</source>
+        <translation>&amp;อีเมล:</translation>
+    </message>
+    <message>
+        <location filename="local/src/activationdialog.cpp" line="35"/>
+        <source>&amp;Code:</source>
+        <translation>&amp;รหัส:</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>&amp;Stop</source>
+        <translation>&amp;หยุด</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&amp;kip</source>
+        <translation>&amp;ข้าม</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>&amp;Pause</source>
+        <translation>&amp;พัก</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>&amp;Full Screen</source>
+        <translation>เ&amp;ต็มจอ</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>&amp;Remove</source>
+        <translation>&amp;ลบออก</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 &amp;Up</source>
+        <translation>ย้าย&amp;ขึ้น</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 &amp;Down</source>
+        <translation>ย้าย&amp;ลง</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>&amp;Quit</source>
+        <translation>&amp;ออก</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>&amp;Website</source>
+        <translation>เ&amp;ว็บไซต์</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>&amp;About</source>
+        <translation>&amp;เกี่ยวกับ</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>&amp;Downloads</source>
+        <translation>&amp;ดาวน์โหลด</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>&amp;Download</source>
+        <translation>&amp;ดาวน์โหลด</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 &amp;Snapshot</source>
+        <translation>&amp;ถ่ายภาพ</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="493"/>
+        <source>&amp;Subscribe to Channel</source>
+        <translation>&amp;สมัครติดตามช่อง</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>&amp;Email</source>
+        <translation>&amp;อีเมล</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>&amp;Close</source>
+        <translation>&amp;ปิด</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="537"/>
+        <source>&amp;Float on Top</source>
+        <translation>&amp;วางอยู่บนสุด</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="542"/>
+        <source>&amp;Stop After This Video</source>
+        <translation>&amp;หยุดหลังจากวิดีโอนี้</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="549"/>
+        <source>&amp;Report an Issue...</source>
+        <translation>&amp;รายงานปัญหา...</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="553"/>
+        <source>&amp;Refine Search...</source>
+        <translation>&amp;ค้นหาโดยละเอียด...</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>&amp;Related Videos</source>
+        <translation>&amp;วิดีโอที่เกี่ยวข้อง</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 &amp;Browser...</source>
+        <translation>เปิดในเ&amp;บราว์เซอร์...</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="583"/>
+        <source>&amp;Love %1? Rate it!</source>
+        <translation>&amp;ชอบ %1 มั้ย? ให้คะแนนมันหน่อย!</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="605"/>
+        <source>&amp;Application</source>
+        <translation>&amp;แอปพลิเคชั่น</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>&amp;Playback</source>
+        <translation>การเ&amp;ล่น</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="633"/>
+        <source>&amp;Playlist</source>
+        <translation>&amp;บัญชีการเล่น</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="642"/>
+        <source>&amp;Video</source>
+        <translation>&amp;วิดีโอ</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="658"/>
+        <source>&amp;View</source>
+        <translation>&amp;มุมมอง</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="665"/>
+        <source>&amp;Share</source>
+        <translation>แ&amp;บ่งปัน</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="679"/>
+        <source>&amp;Help</source>
+        <translation>&amp;ช่วยเหลือ</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 &amp;Full Screen</source>
+        <translation>ออกจากแบบเ&amp;ต็มจอ</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&amp;revious</source>
+        <translation>&amp;ก่อนหน้า</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>&amp;Compact Mode</source>
+        <translation>โหมด&amp;กะทัดรัด</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="324"/>
+        <source>Open the &amp;YouTube Page</source>
+        <translation>เ&amp;ปิดหน้า Youtube</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="331"/>
+        <source>Copy the YouTube &amp;Link</source>
+        <translation>คัดลอก&amp;ลิงค์ Youtube</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="338"/>
+        <source>Copy the Video Stream &amp;URL</source>
+        <translation>คัดลอก &amp;URL ของกระแสวิดีโอ</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="345"/>
+        <source>Find Video &amp;Parts</source>
+        <translation>ค้นหา&amp;ตอนของวิดีโอ</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="373"/>
+        <source>&amp;Clear Recent Searches</source>
+        <translation>&amp;ล้างการค้นหาเมื่อเร็วๆนี้</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="397"/>
+        <source>Make a &amp;Donation</source>
+        <translation>ทำการ&amp;บริจาค</translation>
+    </message>
+    <message>
+        <location filename="src/mainwindow.cpp" line="459"/>
+        <source>&amp;Manually Start Playing</source>
+        <translation>เ&amp;ริ่มการเล่นด้วยตนเอง</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>&amp;Play</source>
+        <translation>เ&amp;ล่น</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 &lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;,</source>
+        <translation>ยินดีต้อนรับสู่ &lt;a href=&apos;%1&apos;&gt;%2&lt;/a&gt;,</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>&quot;Enter&quot;, as in &quot;type&quot;. The whole phrase says: &quot;Enter a keyword to start watching videos&quot;</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>&amp;Back</source>
+        <translation>&amp;กลับ</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
index effa02ed02b9d980f9ccaa1fcaee22cb59f2d20c..2f029bf9c475b3dbeac43c386554d540c1b0d4f9 100644 (file)
@@ -1,6 +1,6 @@
 CONFIG += release
 TEMPLATE = app
-VERSION = 2.3
+VERSION = 2.4
 DEFINES += APP_VERSION="$$VERSION"
 
 APP_NAME = Minitube
@@ -12,14 +12,19 @@ DEFINES += APP_UNIX_NAME="$$APP_UNIX_NAME"
 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
 }
@@ -83,7 +88,6 @@ HEADERS += src/video.h \
     src/gridwidget.h \
     src/painterutils.h \
     src/database.h \
-    src/ytuser.h \
     src/channelaggregator.h \
     src/channelmodel.h \
     src/aggregatevideosource.h \
@@ -93,7 +97,11 @@ HEADERS += src/video.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 \
@@ -149,7 +157,6 @@ SOURCES += src/main.cpp \
     src/gridwidget.cpp \
     src/painterutils.cpp \
     src/database.cpp \
-    src/ytuser.cpp \
     src/channelaggregator.cpp \
     src/channelmodel.cpp \
     src/aggregatevideosource.cpp \
@@ -159,7 +166,11 @@ SOURCES += src/main.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/
index e8e88cc847643e5cd8bb1be4b6dfb243406d8ba0..3cbea11393e7904c62b6e6d2dac9d0c6ad50872a 100644 (file)
@@ -89,7 +89,7 @@ AboutView::AboutView(QWidget *parent) : QWidget(parent) {
             "<p>" + tr("Released under the <a href='%1'>GNU General Public License</a>")
             .arg("http://www.gnu.org/licenses/gpl.html") + "</p>"
         #endif
-            "<p>&copy; 2009-2014 " + Constants::ORG_NAME + "</p>"
+            "<p>&copy; 2009-2015 " + Constants::ORG_NAME + "</p>"
             "</body></html>";
     QLabel *infoLabel = new QLabel(info, this);
     infoLabel->setOpenExternalLinks(true);
index 55e72dcc65e4c54f954ff9f34f176eb177b09d96..2c5f33072188682d5b8fdc67c31473a52c10803f 100644 (file)
@@ -25,9 +25,9 @@ $END_LICENSE */
 
 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,"
@@ -48,7 +48,7 @@ void AggregateVideoSource::loadVideos(int max, int skip) {
         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();
@@ -58,8 +58,8 @@ void AggregateVideoSource::loadVideos(int max, int skip) {
         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());
@@ -67,10 +67,17 @@ void AggregateVideoSource::loadVideos(int max, int skip) {
         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;
index 49de4a0a655af3c2bcc72064bc2032aadaac6caf..55aa730091d02973c353d6255ba8bf38f8d26995 100644 (file)
@@ -30,7 +30,8 @@ class AggregateVideoSource : public VideoSource {
 
 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; }
@@ -40,6 +41,7 @@ public:
 private:
     QString name;
     bool unwatched;
+    bool hasMore;
 
 };
 
index 092a9937fb9c4c0d67f67704c7345da853bf8225..a7eb0553449adb7e0dcd227362c379c346f267ec 100644 (file)
@@ -19,7 +19,7 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "channelaggregator.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #include "ytsearch.h"
 #include "searchparams.h"
 #include "database.h"
@@ -32,8 +32,7 @@ ChannelAggregator::ChannelAggregator(QObject *parent) : QObject(parent),
     unwatchedCount(-1),
     running(false),
     stopped(false) {
-    QSettings settings;
-    checkInterval = settings.value("subscriptionsCheckInterval", 1800).toUInt();
+    checkInterval = 3600;
 
     timer = new QTimer(this);
     timer->setInterval(60000 * 5);
@@ -46,9 +45,10 @@ ChannelAggregator* ChannelAggregator::instance() {
 }
 
 void ChannelAggregator::start() {
+    stopped = false;
     updateUnwatchedCount();
     QTimer::singleShot(0, this, SLOT(run()));
-    timer->start();
+    if (!timer->isActive()) timer->start();
 }
 
 void ChannelAggregator::stop() {
@@ -56,59 +56,65 @@ 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();
@@ -124,8 +130,8 @@ void ChannelAggregator::finish() {
         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);
@@ -138,14 +144,23 @@ void ChannelAggregator::finish() {
     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() {
@@ -177,13 +192,16 @@ void ChannelAggregator::addVideo(Video *video) {
 
     // 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";
@@ -196,13 +214,13 @@ void ChannelAggregator::addVideo(Video *video) {
                   "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());
@@ -216,13 +234,13 @@ void ChannelAggregator::addVideo(Video *video) {
     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);
@@ -232,9 +250,9 @@ void ChannelAggregator::markAllAsWatched() {
     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);
@@ -245,19 +263,19 @@ void ChannelAggregator::videoWatched(Video *video) {
     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();
 
index 1ef224f7644f900737880a244f02b5870002752d..288ce82d70955883cbc9e1d263bf1b6e223e312f 100644 (file)
@@ -23,7 +23,7 @@ $END_LICENSE */
 
 #include <QtCore>
 
-class YTUser;
+class YTChannel;
 class Video;
 
 class ChannelAggregator : public QObject {
@@ -44,16 +44,16 @@ public slots:
     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();
 
@@ -62,7 +62,7 @@ private:
     bool running;
 
     int newVideoCount;
-    QList<YTUser*> updatedChannels;
+    QList<YTChannel*> updatedChannels;
 
     QTimer *timer;
     bool stopped;
index b06e8612c1ad59e78e2b7db68704653178ebdc84..797b82a539a2ea502776ce60a75e0367c4543e3b 100644 (file)
@@ -20,7 +20,7 @@ $END_LICENSE */
 
 #include "channelitemdelegate.h"
 #include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #include "fontutils.h"
 #include "channelaggregator.h"
 #include "painterutils.h"
@@ -94,10 +94,8 @@ void ChannelItemDelegate::paintUnwatched(QPainter* painter,
 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();
 
@@ -108,17 +106,17 @@ void ChannelItemDelegate::paintChannel(QPainter* painter,
     // 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));
 
index e51e4dc2701102715b9ef8723f94f863014b84d6..a2cbd36f4f9b90eea5cfad882fa280fa4e1c073e 100644 (file)
@@ -19,7 +19,7 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "channelmodel.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 
 static const int channelOffset = 2;
 
@@ -40,7 +40,7 @@ QVariant ChannelModel::data(const QModelIndex &index, int role) const {
 
     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:
@@ -48,14 +48,14 @@ QVariant ChannelModel::data(const QModelIndex &index, int role) const {
 
     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);
@@ -85,11 +85,11 @@ void ChannelModel::setQuery(const QString &query, const QSqlDatabase &db) {
         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();
 }
@@ -99,16 +99,16 @@ QSqlError ChannelModel::lastError() const {
 }
 
 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);
@@ -121,11 +121,11 @@ void ChannelModel::updateUnwatched() {
 }
 
 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;
index 6ec5cf1dc7841b42bd680d7c9693ace19d4cb1b6..ee53f7fe1409cac8748a6fc03e71f8c6b1694e71 100644 (file)
@@ -24,7 +24,7 @@ $END_LICENSE */
 #include <QtCore>
 #include <QtSql>
 
-class YTUser;
+class YTChannel;
 
 class ChannelModel : public QAbstractListModel {
 
@@ -48,7 +48,7 @@ public:
     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;
@@ -57,12 +57,12 @@ public:
 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;
 
diff --git a/src/channelsitemdelegate.cpp b/src/channelsitemdelegate.cpp
new file mode 100644 (file)
index 0000000..4f629a8
--- /dev/null
@@ -0,0 +1,152 @@
+#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();
+}
diff --git a/src/channelsitemdelegate.h b/src/channelsitemdelegate.h
new file mode 100644 (file)
index 0000000..afee35d
--- /dev/null
@@ -0,0 +1,24 @@
+#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
diff --git a/src/channelsmodel.cpp b/src/channelsmodel.cpp
new file mode 100644 (file)
index 0000000..4ba3a68
--- /dev/null
@@ -0,0 +1,90 @@
+#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;
+    */
+}
diff --git a/src/channelsmodel.h b/src/channelsmodel.h
new file mode 100644 (file)
index 0000000..02fd5a9
--- /dev/null
@@ -0,0 +1,45 @@
+#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
index c252802f3455e34ce7ba45c0f64da0c1067dd538..95d412abaf6ff67354ec6244eef2fd3cecc3a2a9 100644 (file)
@@ -30,7 +30,7 @@ ChannelSuggest::ChannelSuggest(QObject *parent) : Suggester(parent) {
 }
 
 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;
@@ -51,14 +51,15 @@ void ChannelSuggest::handleNetworkData(QByteArray data) {
     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;
         }
diff --git a/src/channelsview.cpp b/src/channelsview.cpp
new file mode 100644 (file)
index 0000000..11680ca
--- /dev/null
@@ -0,0 +1,275 @@
+#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();
+}
diff --git a/src/channelsview.h b/src/channelsview.h
new file mode 100644 (file)
index 0000000..0988a31
--- /dev/null
@@ -0,0 +1,64 @@
+#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
index af5cb0184f8b57c4caf1f739424209473acbef62..893f891288ee2764f19e98215898023ce0dbd7f5 100644 (file)
@@ -19,7 +19,7 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $END_LICENSE */
 
 #include "channelview.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #include "ytsearch.h"
 #include "searchparams.h"
 #include "channelmodel.h"
@@ -84,8 +84,8 @@ ChannelView::ChannelView(QWidget *parent) : QListView(parent),
 
     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)));
 
@@ -95,7 +95,7 @@ ChannelView::ChannelView(QWidget *parent) : QListView(parent),
 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);
@@ -174,10 +174,11 @@ void ChannelView::appear() {
     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);
 }
@@ -208,14 +209,15 @@ void ChannelView::itemEntered(const QModelIndex &index) {
 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"));
@@ -232,15 +234,15 @@ void ChannelView::showContextMenu(const QPoint &point) {
     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();
@@ -253,7 +255,7 @@ void ChannelView::showContextMenu(const QPoint &point) {
     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()));
 
diff --git a/src/channelwidget.cpp b/src/channelwidget.cpp
new file mode 100644 (file)
index 0000000..ca227b1
--- /dev/null
@@ -0,0 +1,85 @@
+#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();
+}
diff --git a/src/channelwidget.h b/src/channelwidget.h
new file mode 100644 (file)
index 0000000..7ceb6ae
--- /dev/null
@@ -0,0 +1,35 @@
+#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
index 3e7c537d5970aadeb2c1f607fa804fee03d4926b..4e089ff9cdc895d812aabadf81be0470e0a93f05 100644 (file)
@@ -38,11 +38,15 @@ Database::Database() {
 
     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();
 }
 
@@ -58,9 +62,9 @@ void Database::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,"
@@ -77,13 +81,13 @@ void Database::createDatabase() {
     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,"
@@ -156,11 +160,31 @@ QVariant Database::getAttribute(QString name) {
 
 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__;
 }
 
 /**
index 74ab8b710ba19d2a5e0bb68f7aac265935ebbf78..e5133fca18994e7931fe7d262bd443872617256d 100644 (file)
@@ -45,6 +45,8 @@ private:
     QVariant getAttribute(QString name);
     void setAttribute(QString name, QVariant value);
 
+    void fixChannelIds();
+
     QMutex lock;
     QString dbLocation;
     QHash<QThread*, QSqlDatabase> connections;
index 3490448266e1a13be0df027c841b1c6e1c1b1286..11bb943e231b00c3adde1c7799c9876031a36064 100644 (file)
@@ -20,3 +20,60 @@ QString DataUtils::stringToFilename(const QString &s) {
 
     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;
+}
index eb0a3c0c6035625a665fd439598f96f6ef089435..e5a8de08cbe3ec761876f14df349173d17c18876 100644 (file)
@@ -7,6 +7,9 @@ class DataUtils {
 
 public:
     static QString stringToFilename(const QString &s);
+    static QString regioneCode(const QLocale &locale);
+    static QString systemRegioneCode();
+    static uint parseIsoPeriod(const QString &isoPeriod);
 
 private:
     DataUtils() { }
index f9d0391b45c4b86700a46c268854f8b253ffa6a1..4766b008635f3c5c8dc988914dfd35ae389fe939 100644 (file)
@@ -25,7 +25,7 @@ DiskCache::DiskCache(QObject *parent) : QNetworkDiskCache(parent) { }
 
 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;
@@ -33,9 +33,20 @@ QIODevice* DiskCache::prepare(const QNetworkCacheMetaData &metaData) {
         }
     }
 
-    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);
+}
index 1f3cdc911a59517508a82581d5a5cd640a5d7530..c79a71b146e27242f7da7e6d1fa18dfa63158fcf 100644 (file)
@@ -29,6 +29,7 @@ class DiskCache : public QNetworkDiskCache
 public:
     explicit DiskCache(QObject *parent = 0);
     QIODevice* prepare(const QNetworkCacheMetaData &metaData);
+    QNetworkCacheMetaData metaData(const QUrl &url);
 
 signals:
 
index b23098f6a4ea2920f740c7f08dc9d747933d5930..0d39dfbf5bc8e6774c393919d22941b4d60043c3 100644 (file)
@@ -28,20 +28,20 @@ NetworkAccess* http();
 }
 
 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);
@@ -52,50 +52,56 @@ JsFunctions::JsFunctions(QObject *parent) : QObject(parent), engine(0) {
 
 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())
@@ -103,41 +109,63 @@ QString JsFunctions::evaluate(const QString &js) {
     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()");
 }
index e623a4b5c7dcc04d5e2793cade85da3b7ca9313b..174e6ff4506ac278cbef48ec99bc0cd07be8a67f 100644 (file)
@@ -31,6 +31,10 @@ class JsFunctions : public QObject {
 
 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
@@ -43,21 +47,22 @@ public:
     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;
 };
 
index 91f2ed87e20b887956dfd0203395850c5add7780..131c17cb664a031c7ec0f676ab1a6319d2815fa6 100644 (file)
@@ -34,11 +34,37 @@ $END_LICENSE */
 #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);
@@ -96,39 +122,16 @@ int main(int argc, char **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();
 }
index 27959971075e608e00f190ac0251165fcc37d8d9..08a20a64087bff3f5e8f14c279454a921e03b468 100644 (file)
@@ -70,11 +70,15 @@ $END_LICENSE */
 #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;
 }
 
@@ -92,6 +96,10 @@ MainWindow::MainWindow() :
 
     singleton = this;
 
+#ifdef APP_EXTRA
+    Extra::windowSetup(this);
+#endif
+
     // views mechanism
     history = new QStack<QWidget*>();
     views = new QStackedWidget();
@@ -139,10 +147,6 @@ MainWindow::MainWindow() :
 
     views->show();
 
-#ifdef APP_EXTRA
-    Extra::windowSetup(this);
-#endif
-
     qApp->processEvents();
     QTimer::singleShot(50, this, SLOT(lazyInit()));
 }
@@ -204,8 +208,6 @@ void MainWindow::lazyInit() {
     JsFunctions::instance();
 
     checkForUpdate();
-
-    ChannelAggregator::instance()->start();
 }
 
 void MainWindow::changeEvent(QEvent* event) {
@@ -230,7 +232,7 @@ bool MainWindow::eventFilter(QObject *obj, 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);
@@ -438,7 +440,7 @@ void MainWindow::createActions() {
     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
@@ -750,6 +752,10 @@ void MainWindow::createToolBars() {
 
     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);
@@ -978,7 +984,7 @@ void MainWindow::quit() {
     if (!m_fullscreen && !compactViewAct->isChecked()) {
         writeSettings();
     }
-    mediaView->stop();
+    // mediaView->stop();
     Temporary::deleteAll();
     ChannelAggregator::instance()->stop();
     ChannelAggregator::instance()->cleanup();
@@ -1310,20 +1316,25 @@ void MainWindow::initPhonon() {
     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
 
@@ -1401,15 +1412,20 @@ void MainWindow::volumeDown() {
 
 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;
@@ -1437,6 +1453,10 @@ void MainWindow::volumeMutedChanged(bool muted) {
         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) {
index 1f703181f425f850e451912a19487c3a1c5dda53..c1617f1e060c991ca029c2946f71fde9aae7523c 100644 (file)
@@ -45,7 +45,7 @@ $END_LICENSE */
 #include "ytsinglevideosource.h"
 #include "channelaggregator.h"
 #include "iconutils.h"
-#include "ytuser.h"
+#include "ytchannel.h"
 #ifdef APP_SNAPSHOT
 #include "snapshotsettings.h"
 #endif
@@ -205,7 +205,10 @@ void MediaView::search(SearchParams *searchParams) {
             }
         }
     }
-    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) {
@@ -245,7 +248,7 @@ void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool
     sidebar->getHeader()->updateInfo();
 
     SearchParams *searchParams = getSearchParams();
-    bool isChannel = searchParams && !searchParams->author().isEmpty();
+    bool isChannel = searchParams && !searchParams->channelId().isEmpty();
     playlistView->setClickableAuthors(!isChannel);
 
 
@@ -315,7 +318,7 @@ void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/)
     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());
     }
@@ -446,7 +449,7 @@ void MediaView::activeRowChanged(int row) {
     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);
@@ -488,7 +491,7 @@ void MediaView::gotStreamUrl(QUrl streamUrl) {
     startDownloading();
 #endif
 
-    // ensure we always have 10 videos ahead
+    // ensure we always have videos ahead
     playlistModel->searchNeeded();
 
     // ensure active item is visible
@@ -504,7 +507,7 @@ void MediaView::gotStreamUrl(QUrl streamUrl) {
 #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);
@@ -676,7 +679,7 @@ void MediaView::openWebPage() {
 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);
@@ -845,6 +848,8 @@ void MediaView::snapshot() {
     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) + ")";
@@ -977,7 +982,7 @@ void MediaView::findVideoParts() {
     SearchParams *searchParams = new SearchParams();
     searchParams->setTransient(true);
     searchParams->setKeywords(query);
-    searchParams->setAuthor(video->author());
+    searchParams->setChannelId(video->channelId());
 
     /*
     if (!numberAsWords) {
@@ -996,7 +1001,8 @@ void MediaView::relatedVideos() {
     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);
 }
@@ -1012,7 +1018,7 @@ void MediaView::shareViaTwitter() {
 #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);
     }
@@ -1030,7 +1036,7 @@ void MediaView::shareViaFacebook() {
         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);
     }
@@ -1049,7 +1055,7 @@ void MediaView::shareViaBuffer() {
 #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);
@@ -1069,7 +1075,7 @@ void MediaView::shareViaEmail() {
 #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);
@@ -1084,12 +1090,12 @@ void MediaView::authorPushed(QModelIndex index) {
     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!
@@ -1105,11 +1111,11 @@ void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
         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);
     }
@@ -1138,10 +1144,10 @@ void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
 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);
 }
index 7f2c33414692fee029552da64858312127e6e368..9b99e467781c57adaa0e286c601614289a9577c1 100644 (file)
@@ -74,7 +74,7 @@ void NetworkReply::finished() {
             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)
diff --git a/src/paginatedvideosource.cpp b/src/paginatedvideosource.cpp
new file mode 100644 (file)
index 0000000..396b836
--- /dev/null
@@ -0,0 +1,170 @@
+/* $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();
+    }
+}
diff --git a/src/paginatedvideosource.h b/src/paginatedvideosource.h
new file mode 100644 (file)
index 0000000..1c04b52
--- /dev/null
@@ -0,0 +1,59 @@
+/* $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
index 8dcfdf4aebbf226ab0ba898cc60add9c7e52f17c..54f4cbdab59491c7b8c81ae375c166e398ae4dc6 100644 (file)
@@ -25,7 +25,7 @@ PainterUtils::PainterUtils() { }
 
 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,
index 81bdae47612e9e13789d06d7ce8eb47ce01e395d..717c7e57d64cbe7ee82a8318bd552d645a8bb41d 100644 (file)
@@ -131,7 +131,8 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
         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));
@@ -142,7 +143,7 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
 
     if (line.width() > THUMB_WIDTH + 60) {
 
-        if (isActive) painter->setFont(boldFont);
+        // if (isActive) painter->setFont(boldFont);
 
         // text color
         if (isSelected)
@@ -192,7 +193,7 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
                 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);
@@ -226,7 +227,7 @@ void PlaylistItemDelegate::paintBody( QPainter* painter,
 
     } 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);
index a3cad088bcee9646d686264c1ab6bdd25e1cd457..eb8a2ddb06ffc808244ee45887bb8452894ebdcc 100644 (file)
@@ -26,7 +26,7 @@ $END_LICENSE */
 #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";
 
@@ -37,7 +37,7 @@ PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
     firstSearch = false;
     m_activeVideo = 0;
     m_activeRow = -1;
-    skip = 1;
+    startIndex = 1;
     max = 0;
     hoveredRow = -1;
     authorHovered = false;
@@ -70,7 +70,7 @@ QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
         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:
@@ -165,11 +165,11 @@ Video* PlaylistModel::activeVideo() const {
 
 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;
@@ -186,11 +186,11 @@ void PlaylistModel::setVideoSource(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() {
@@ -198,27 +198,29 @@ 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 ) );
@@ -243,8 +245,9 @@ void PlaylistModel::addVideos(QList<Video*> newVideos) {
     endInsertRows();
     foreach (Video* video, newVideos) {
         connect(video, SIGNAL(gotThumbnail()),
-                SLOT(updateThumbnail()), Qt::UniqueConnection);
+                SLOT(updateVideoSender()), Qt::UniqueConnection);
         video->loadThumbnail();
+        qApp->processEvents();
     }
 }
 
@@ -288,15 +291,15 @@ void PlaylistModel::handleFirstVideo(Video *video) {
         }
 
         // 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();
@@ -305,17 +308,19 @@ void PlaylistModel::handleFirstVideo(Video *video) {
     }
 }
 
-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
@@ -366,7 +371,7 @@ Qt::DropActions PlaylistModel::supportedDragActions() const {
 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);
     }
@@ -439,8 +444,10 @@ bool PlaylistModel::dropMimeData(const QMimeData *data,
 }
 
 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;
@@ -496,8 +503,13 @@ void PlaylistModel::setHoveredRow(int row) {
 }
 
 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 */
@@ -505,28 +517,24 @@ void PlaylistModel::clearHover() {
 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();
 }
index 88d6dcfea7601e24bfa3bb6a17449e9774485f21..2123bd245d603b29c14ddc7f821c076044abee75 100644 (file)
@@ -91,15 +91,17 @@ public slots:
     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);
@@ -116,7 +118,7 @@ private:
     bool firstSearch;
 
     QList<Video*> videos;
-    int skip;
+    int startIndex;
     int max;
 
     int m_activeRow;
@@ -127,6 +129,8 @@ private:
     int hoveredRow;
     bool authorHovered;
     bool authorPressed;
+
+    QMutex mutex;
 };
 
 #endif
index 2391f9fe6258f019f424d497c8846631cb6842e3..c831c3be5007cac9576a1545f84c05c7dd3de2dc 100644 (file)
@@ -54,7 +54,8 @@ void PlaylistView::itemEntered(const QModelIndex &index) {
     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();
 }
index be08e72241a6b6e2a2b46afa7d03b4b73cb13c44..17a8dfa68247dd4b86afc236590c8e9c44aa3c65 100644 (file)
@@ -26,6 +26,7 @@ SearchParams::SearchParams(QObject *parent) : QObject(parent) {
     m_duration = DurationAny;
     m_quality = QualityAny;
     m_time = TimeAny;
+    m_publishedAfter = 0;
 }
 
 void SearchParams::setParam(QString name, QVariant value) {
index f2e9d035ec98839842a12265a6fd3c5694b02d77..f55dcebaf2e1641dd97f9d5a3fa0a5f8e3777f94 100644 (file)
@@ -62,10 +62,10 @@ public:
     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; }
@@ -82,9 +82,12 @@ public:
     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:
@@ -92,12 +95,13 @@ 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;
 
 };
 
index 2e3ac2769a846007bee9ff583a42927c1b2ef8ab..a0037a3d48a0b71690ed0bd59a9e6d073a49d5b5 100644 (file)
@@ -91,7 +91,7 @@ SearchView::SearchView(QWidget *parent) : QWidget(parent) {
                             #if defined(APP_UBUNTU) || defined(APP_WIN)
                                 "normal"
                             #else
-                                "bold"
+                                "normal"
                             #endif
                                 "' ")
                        .arg(Constants::WEBSITE, Constants::NAME)
@@ -198,12 +198,14 @@ SearchView::SearchView(QWidget *parent) : QWidget(parent) {
 }
 
 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() {
@@ -318,7 +320,8 @@ void SearchView::watch(QString query) {
     else {
         // remove spaces from channel name
         query = query.simplified();
-        searchParams->setAuthor(query);
+        query = query.remove(' ');
+        searchParams->setChannelId(query);
         searchParams->setSortBy(SearchParams::SortByNewest);
     }
 
@@ -326,21 +329,19 @@ void SearchView::watch(QString query) {
     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!
@@ -402,5 +403,7 @@ void SearchView::searchTypeChanged(int index) {
 }
 
 void SearchView::suggestionAccepted(Suggestion *suggestion) {
-    watch(suggestion->value);
+    if (suggestion->type == QLatin1String("channel")) {
+        watchChannel(suggestion->userData);
+    } else watch(suggestion->value);
 }
index f513c61e9e9f70fd9ed548eb22cab948d637a51c..a8022b6f724fcfc4ce3d6780d9f1ed313c58e397 100644 (file)
@@ -46,7 +46,7 @@ public slots:
     void appear();
     void disappear() { }
     void watch(QString query);
-    void watchChannel(QString channel);
+    void watchChannel(const QString &channelId);
     void watchKeywords(QString query);
 
 signals:
index d9b31ae74caa8c0a86f3ebc8e599b8952cea0c69..116286b123aa3c9a662d70e8a65d0cfe898c4c52 100644 (file)
@@ -24,6 +24,7 @@ $END_LICENSE */
 #ifdef APP_MAC
 #include "macutils.h"
 #endif
+#include "constants.h"
 
 SnapshotSettings::SnapshotSettings(QWidget *parent) : QWidget(parent) {
     QBoxLayout *layout = new QHBoxLayout(this);
@@ -60,8 +61,8 @@ void SnapshotSettings::setSnapshot(const QPixmap &pixmap, const QString &filenam
     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);
 }
 
@@ -78,6 +79,9 @@ QString SnapshotSettings::getCurrentLocation() {
         location = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
 #else
         location = QDesktopServices::storageLocation(QDesktopServices::PicturesLocation);
+#endif
+#ifdef APP_MAC_STORE
+        location += "/MinitubeforYouTube";
 #endif
     }
     return location;
index 7942d0d77dce2e9084c3d922962a873e292fd9fe..0f0356493d88a3f8cb5dc14046c08cc8aaa0ca1c 100644 (file)
@@ -50,6 +50,7 @@ StandardFeedsView::StandardFeedsView(QWidget *parent) : QWidget(parent),
 }
 
 void StandardFeedsView::load() {
+    setUpdatesEnabled(false);
     YTCategories *youTubeCategories = new YTCategories(this);
     connect(youTubeCategories, SIGNAL(categoriesLoaded(const QList<YTCategory> &)),
             SLOT(layoutCategories(const QList<YTCategory> &)));
@@ -88,6 +89,7 @@ void StandardFeedsView::layoutCategories(const QList<YTCategory> &categories) {
         feed->setFeedId("most_popular");
         addVideoSourceWidget(feed);
     }
+    if (categories.size() > 1) setUpdatesEnabled(true);
 }
 
 void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
@@ -124,7 +126,11 @@ YTStandardFeed* StandardFeedsView::buildStardardFeed(
 
 void StandardFeedsView::appear() {
     setFocus();
-    if (!layout) load();
+    if (!layout) {
+        update();
+        qApp->processEvents();
+        load();
+    }
     QAction *regionAction = MainWindow::instance()->getRegionAction();
     regionAction->setVisible(true);
 }
@@ -146,6 +152,6 @@ void StandardFeedsView::selectLocalRegion() {
 
 void StandardFeedsView::paintEvent(QPaintEvent *event) {
     QWidget::paintEvent(event);
-    PainterUtils::topShadow(this);
+    // PainterUtils::topShadow(this);
 }
 
index cca7f5f1041e5f11fc874ec3812800ddb4d1b6fe..37b805cc7883c099332f1ba13083231469769a9b 100644 (file)
@@ -27,10 +27,12 @@ class Suggestion {
 
 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);
index e7c90da4464d06b3bbaf85be11e6f8b20d1ad952..29b74b8125731e125886a0de2e4dc9a3beb99c90 100644 (file)
@@ -40,15 +40,15 @@ Video::Video() : m_duration(0),
     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;
@@ -63,18 +63,26 @@ Video* Video::clone() {
     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() {
@@ -86,6 +94,7 @@ 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);
@@ -117,7 +126,7 @@ void  Video::getVideoInfo() {
 
     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
         {
@@ -144,7 +153,7 @@ void  Video::getVideoInfo() {
     } 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)));
     }
 
@@ -162,7 +171,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     // 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();
@@ -170,7 +179,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     }
 
     QString videoToken = videoTokeRE.cap(1);
-    // qWarning() << "got token" << videoToken;
+    // qDebug() << "got token" << videoToken;
     while (videoToken.contains('%'))
         videoToken = QByteArray::fromPercentEncoding(videoToken.toLatin1());
     // qDebug() << "videoToken" << videoToken;
@@ -179,7 +188,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     // 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();
@@ -189,7 +198,7 @@ void  Video::gotVideoInfo(QByteArray data) {
     // 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);
 }
@@ -236,7 +245,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
                             sig = JsFunctions::instance()->decryptSignature(sig);
                     }
                 } else {
-                    // qDebug() << "Loading webpage";
+
                     QUrl url("http://www.youtube.com/watch");
 
 #if QT_VERSION >= 0x050000
@@ -252,6 +261,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
                         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*)));
@@ -270,7 +280,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
         // 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;
@@ -290,7 +300,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
         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;
@@ -302,7 +312,7 @@ void Video::parseFmtUrlMap(const QString &fmtUrlMap, bool fromWebPage) {
         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) {
@@ -312,7 +322,6 @@ 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) {
@@ -325,7 +334,7 @@ void Video::scrapeWebPage(QByteArray data) {
 
     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++;
@@ -339,7 +348,7 @@ void Video::scrapeWebPage(QByteArray data) {
 #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);
@@ -356,10 +365,10 @@ void Video::scrapeWebPage(QByteArray data) {
         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*)));
@@ -391,16 +400,18 @@ void Video::parseJsPlayer(QByteArray bytes) {
             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;
         }
@@ -411,7 +422,7 @@ void Video::parseJsPlayer(QByteArray bytes) {
 }
 
 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);
index 3a85233bb82ad5bb111094d8846afd89dbfbcbf9..16454425a87463b3038b413a4b548ce6e1860281 100644 (file)
@@ -41,29 +41,29 @@ public:
     };
 
     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; }
@@ -73,7 +73,7 @@ public:
     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; }
 
@@ -109,9 +109,9 @@ private:
 
     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;
index 760f512c0063aa280bafb54e622f884ca3cdcbfc..458d120381b111eb588a0820b39730af488e3c69 100644 (file)
@@ -32,7 +32,8 @@ class VideoSource : public QObject {
 
 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;
diff --git a/src/yt3.cpp b/src/yt3.cpp
new file mode 100644 (file)
index 0000000..c341310
--- /dev/null
@@ -0,0 +1,119 @@
+#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;
+    }
+}
diff --git a/src/yt3.h b/src/yt3.h
new file mode 100644 (file)
index 0000000..124409f
--- /dev/null
+++ b/src/yt3.h
@@ -0,0 +1,32 @@
+#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
diff --git a/src/yt3listparser.cpp b/src/yt3listparser.cpp
new file mode 100644 (file)
index 0000000..ed65c72
--- /dev/null
@@ -0,0 +1,76 @@
+#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);
+}
diff --git a/src/yt3listparser.h b/src/yt3listparser.h
new file mode 100644 (file)
index 0000000..6ce915c
--- /dev/null
@@ -0,0 +1,25 @@
+#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
index 89217420c91ca9552977a5134a661d1a63058961..db5a244cddd770c1a2b4605075af0dab18de357f 100644 (file)
@@ -20,7 +20,12 @@ $END_LICENSE */
 
 #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();
@@ -33,12 +38,72 @@ void YTCategories::loadCategories(QString language) {
         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;
 
@@ -66,6 +131,8 @@ void YTCategories::parseCategories(QByteArray bytes) {
     emit categoriesLoaded(categories);
 }
 
+#endif
+
 void YTCategories::requestError(QNetworkReply *reply) {
     if (lastLanguage != "en") loadCategories("en");
     else emit error(reply->errorString());
diff --git a/src/ytchannel.cpp b/src/ytchannel.cpp
new file mode 100644 (file)
index 0000000..ad65241
--- /dev/null
@@ -0,0 +1,387 @@
+/* $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;
+}
diff --git a/src/ytchannel.h b/src/ytchannel.h
new file mode 100644 (file)
index 0000000..25a9fca
--- /dev/null
@@ -0,0 +1,111 @@
+/* $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
index 9aaacf0a1d1209b678dec7d1270aa3d8b7430ac5..c063ce243999643b2b7727a4a7118eda64e5d3c6 100644 (file)
@@ -56,15 +56,15 @@ void YTFeedReader::readEntry() {
                     ) {
                 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));
index 195c2a52b277fb612207a199d9e2e492057bbf50..e7784a1b2f6d58781c39dc02587ae3ff48d6e023 100644 (file)
@@ -21,7 +21,7 @@ $END_LICENSE */
 #ifndef YTFEEDREADER_H
 #define YTFEEDREADER_H
 
-#include <QtXml>
+#include <QtCore>
 
 class Video;
 
index 58f7a021031b0651142da1c174f8b0f7bdbe64e0..331a791d9e595b06e7e1e399fe41bc278b1d4727 100644 (file)
@@ -19,111 +19,239 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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) {
@@ -133,12 +261,12 @@ 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);
     }
@@ -147,7 +275,24 @@ void YTSearch::parseResults(QByteArray data) {
     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());
 }
 
@@ -161,7 +306,7 @@ QString YTSearch::videoIdFromUrl(QString url) {
 
 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;
index 7ed282cc7ffbaed7ae6e8420947b1ca5a5205e8b..be2a0c162ed742b33335b1c182fe72f59c98756b 100644 (file)
@@ -22,30 +22,29 @@ $END_LICENSE */
 #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);
@@ -55,8 +54,6 @@ private:
     bool aborted;
     QStringList suggestions;
     QString name;
-
-    QString userId;
 };
 
 #endif // YTSEARCH_H
index 05e5a327db238eae33b1654fb10c170898cfee8b..6d9276cb3d9b0c26ed47c3e7b0b6616cefd0dc71 100644 (file)
@@ -19,40 +19,128 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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);
@@ -63,37 +151,44 @@ void YTSingleVideoSource::loadVideos(int max, int skip) {
     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());
 }
index 53dfd529edb9018eb6e30a2fba025d73e8cd9c41..dc869a6edbe7c2c6f657fb401d206b2c4d38de5d 100644 (file)
@@ -22,29 +22,31 @@ $END_LICENSE */
 #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;
 };
index ee5e0030f141a8982754fd5840eec756f7eed638..59ce7304c857ef111a10c6209ef9bc24545a55c2 100644 (file)
@@ -19,20 +19,85 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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/";
@@ -42,20 +107,20 @@ void YTStandardFeed::loadVideos(int max, int skip) {
 
     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);
@@ -66,16 +131,7 @@ void YTStandardFeed::loadVideos(int max, int skip) {
     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);
@@ -85,6 +141,17 @@ void YTStandardFeed::parse(QByteArray 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());
 }
index 13979a8dca9d4acc6d78e5d968d6b2e0d8e63745..b1b64e9986e16c082397c18746fa9d997fcb4af3 100644 (file)
@@ -22,9 +22,9 @@ $END_LICENSE */
 #define YTSTANDARDFEED_H
 
 #include <QtNetwork>
-#include "videosource.h"
+#include "paginatedvideosource.h"
 
-class YTStandardFeed : public VideoSource {
+class YTStandardFeed : public PaginatedVideoSource {
 
     Q_OBJECT
 
@@ -46,13 +46,13 @@ public:
     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:
index a11824e7fbec0ea29fe5cea2c4e3177f68391443..86be974049e66aeb912865224e466cc7f4c8f3fe 100644 (file)
@@ -19,13 +19,10 @@ along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
 $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) {
@@ -46,7 +43,9 @@ void YTSuggester::suggest(const QString &query) {
         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)));
diff --git a/src/ytuser.cpp b/src/ytuser.cpp
deleted file mode 100644 (file)
index f51bd18..0000000
+++ /dev/null
@@ -1,328 +0,0 @@
-/* $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;
-}
diff --git a/src/ytuser.h b/src/ytuser.h
deleted file mode 100644 (file)
index 306cd13..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/* $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