Unify domain names in README master github/master
authorChristian Weiske <cweiske@cweiske.de>
Wed, 31 Jan 2024 18:00:39 +0000 (19:00 +0100)
committerChristian Weiske <cweiske@cweiske.de>
Wed, 31 Jan 2024 18:00:55 +0000 (19:00 +0100)
33 files changed:
.gitignore
HOWTO-SETUP.rst [new file with mode: 0644]
README.rst
bin/build-html.php
bin/clean-generated-and-qr.sh [new file with mode: 0755]
bin/clean-generated-keep-qr.sh [new file with mode: 0755]
bin/clean-generated.sh [deleted file]
bin/create-qr.sh [new file with mode: 0755]
bin/filters.php
bin/functions.php
bin/import-game-data.php
config.php.dist
data/templates/allgames.tpl.php
data/templates/discover.tpl.php
data/templates/game.tpl.php
www/.htaccess
www/agreements/marketplace.html [new file with mode: 0644]
www/api/firmware_builds
www/api/razer/session [new file with mode: 0644]
www/api/razer/session.php [new file with mode: 0644]
www/api/v1/console_configuration
www/api/v1/gamers/me.json [new file with mode: 0644]
www/api/v1/gamers/me.php [new file with mode: 0644]
www/api/v1/gamers/me/index.html [deleted file]
www/api/v1/gamers/register-error.json [new file with mode: 0644]
www/api/v1/partner_builds.json [new file with mode: 0644]
www/api/v1/partner_builds.razer-forge-tv.json [new file with mode: 0644]
www/api/v1/partner_builds/release_notes [new file with mode: 0644]
www/api/v1/queued_downloads_delete.php
www/api/v1/sessions
www/api/v1/sessions.php [new file with mode: 0644]
www/ouya-discover.css
www/updates-ouya_1_1.json [new file with mode: 0644]

index 4827cbd838a87ed6412e092bd2d1ef32a0d974d1..aadae73f429ed3d2705d42a08174ac000bbedb99 100644 (file)
@@ -1,5 +1,6 @@
 /config.php
 /data/push-to-my-ouya.sqlite3
+/data/usernames.sqlite3
 /README.html
 www/api/v1/apps/
 www/api/v1/details-data/
@@ -10,3 +11,5 @@ www/api/v1/games/*/
 www/api/v1/search-data/
 www/discover/
 www/game/
+www/gen-qr/
+www/game-data-version
diff --git a/HOWTO-SETUP.rst b/HOWTO-SETUP.rst
new file mode 100644 (file)
index 0000000..191c830
--- /dev/null
@@ -0,0 +1,317 @@
+======================================
+Building and enabling a local stouyapi
+======================================
+
+The whole procedure was done on Linux, using Pop-OS, release Pop!_OS 22.04 LTS.
+The configuration will be the same for any Ubuntu/Debian based distro.
+
+NOTE: Commands with the $ symbol mean they should be run with your default user.
+Commands with # means they must be run as root.
+
+
+1 - Installing the dependencies:
+================================
+
+To build the stouyapi API and HTML files on Pop-OS you need to install the
+following packages:
+
+- imagemagick
+- exiftool
+- qrencode
+- ttf-mscorefonts-installer (*)
+
+(*) Pop-OS complained about missing helvetica font, so this package is needed.
+I haven't tested it with another Helvetica compatible font package. This package
+just installs the actual font installer. A screen will appear asking you to
+accept the fonts EULA and continue with the installation.
+
+To run the server, you need to install the following packages:
+
+- apache
+- php (*)
+- php-sqlite
+
+(*) When installed, it already activates the necessary module in apache.
+
+To install the packages on Pop-OS, just use the following command::
+
+    # apt install imagemagick exiftool qrencode ttf-mscorefonts-installer apache2 php php-sqlite3
+
+**ATTENTION: The above listing is not definitive and may vary if you use another
+distro such as Fedora, CentOS, etc. Make sure you have the package installed on
+your distribution.**
+
+
+2 - Building the API and HTML files:
+====================================
+
+First download the stouyapi code and files to your computer.
+
+In a terminal, type::
+
+    $ git clone https://github.com/cweiske/stouyapi.git
+
+This will create the stouyapi directory.
+
+Now enter in the stouyapi directory and download the ouya-game-data code and files::
+
+    $ cd stouyapi
+    $ git clone https://github.com/ouya-saviors/ouya-game-data.git
+
+**ATTENTION: If you want to add a game/program to your local store, now is the time:
+Just include the json file of the game/program in the "ouya-game-data/new" folder,
+before importing the data.**
+
+Now, before creating the API files and HTML files, you must rename and, if you wish,
+edit the config.php.dist file.
+
+This file:
+
+- Changes all links pointing to the archive.org site to point to static.ouya.world;
+- Configures the list of indicated games that appears on the OUYA home screen (where we have the options PLAY, DISCOVER, etc.);
+- Configures a list of suggested games, which appears within DISCOVER, below the list of new games.
+
+Rename the config.php.dist file to config.php::
+
+    $ cp config.php.dist config.php
+
+If you want to edit it, open it with nano or any text editor of your choice::
+
+    $ nano config.php
+
+You should only change the following two sections:
+
+The first section is personal game recommendations within DISCOVER, below the new games listing::
+
+    $GLOBALS['packagelists']["cweiske's picks"] = [
+             'de.eiswuxe.blookid2',
+             'com.cosmos.babylonantwins',
+             'com.inverseblue.skyriders',
+    ];
+
+If you want:
+
+- Change the title, cweiske's picks, keeping the double quotes,
+- Change/include a game by informing the name of the game's json file between single quotes and a comma at the end, following the same formatting as above. If you want to Delete a game, just delete the line.
+
+The session below are indications of games that appear on the OUYA home screen::
+
+    $GLOBALS['home']['2020 Winter GameJam'] = [
+        'com.DESKINK.ToneTests',
+        'com.Eightbbgames.yahayor',
+        'com.FuzzyPopcorn',
+        'com.NYYLE.NYCTO',
+        'com.NoelRojasOliveras.PaintKiller',
+        'com.StrawHat.Fall',
+        'com.oliverstogden.trf',
+        'com.samuelsousa.shootdestroy',
+        'com.scorpion.shootout',
+        'com.sd_game_dev.aliens_taste_my_sword',
+        'com.sumotown.sirtet',
+        'de.x2041.games.gyrogun',
+        'ht.sr.git.arcticGrind.embed',
+        'tv.ouya.demo.DarkSpacePioneer',
+        'tv.ouya.win.unity.brokenbeauty',
+    ];
+
+Edit in the same way, but note that on the home screen the title of the recommendations,
+2020 Winter GameJam, is enclosed in single quotes.
+
+Do not change any other field in the file and after making changes, save it.
+
+Now generate the API files::
+
+    $ ./bin/import-game-data.php ouya-game-data/folders
+
+Creating the files takes a while. Wait to finish.
+
+When finished, create the HTML files::
+
+    $ ./bin/build-html.php
+
+
+3 - Setting up the site
+========================
+
+So far, apache is already running. If you type in the browser http://localhost the default
+apache website will appear. Now let's create the settings for the STOUYAPI.
+
+In the terminal, type::
+
+    $ cd /etc/apache2/sites-available/
+
+Now, copy the apache default site file and rename it however you want but keep the ".conf"
+extension. I left it with the name of stouyapi::
+
+    # cp 000-default.conf stouyapi.conf
+
+The file we copied is a file with minimal apache default settings for virtual hosts.
+
+Now let's edit it with nano::
+
+    # nano stouyapi.conf
+
+Now, look for the line that looks like below::
+
+    #ServerName www.example.com
+
+It tells apache the address of the site. Uncomment it (remove the #) and change the address
+to whatever you like. Here I left it like this::
+
+    ServerName stouyapi.local
+
+Now find a line that looks like below::
+
+    DocumentRoot /var/www/html
+
+That line basically tells apache where the site's files are.
+I chose to leave my files in the following path::
+
+    DocumentRoot /srv/stouyapi/www
+
+**ATTENTION: You can use any directory name you want, but
+remember that the path you enter must be complete until the
+folder that contains the files and folders on the server.
+They are all those that are inside the www directory, inside
+the stouyapi folder where we generate the API files and HTML files.**
+
+Now let's go to the end of the file, and before the line below::
+
+    </VirtualHost>
+
+Include the following lines::
+
+    Script PUT /empty-json.php
+    Script DELETE /api/v1/queued_downloads_delete.php
+
+    <Directory /srv/stouyapi/www>
+        AllowOverride All
+        Require all granted
+    </Directory>
+
+**ATTENTION: Pay attention that the path in "DocumentRoot" and "<Directory>" should be the same.**
+
+In the end, disregarding all the comment lines that the file has, it will look like this::
+
+       <VirtualHost *:80>
+
+               ServerName stouyapi.local
+
+               ServerAdmin webmaster@localhost
+               DocumentRoot /srv/stouyapi/www
+
+               ErrorLog ${APACHE_LOG_DIR}/error.log
+               CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+               Script PUT /empty-json.php
+               Script DELETE /api/v1/queued_downloads_delete.php
+
+               <Directory /srv/stouyapi/www>
+                       AllowOverride All
+                       Require all granted
+               </Directory>
+
+       </VirtualHost>
+
+Save the file and close.
+
+Now let's move the site files to the location indicated in the configuration file.
+
+Do::
+
+    # mkdir /srv/stouyapi
+
+Then go inside the stouyapi folder where we created the API and HTML files and do::
+
+    # cp -R www /srv/stouyapi
+
+This will copy the www folder to /srv/stouyapi.
+
+You can check with the following command::
+
+    $ ls /srv/stouyapi
+
+Which will return the www folder.
+
+
+4 - Activating the apache modules and the website.
+==================================================
+
+With the configuration file created and the site files in place, let's activate the modules and the site.
+
+First the modules, enter the following command::
+
+    # a2enmod actions expires php8.1 rewrite
+
+This will activate the necessary modules. Don't worry if any of them are already active
+(php8.1 will be), as apache just tells you that it's already configured.
+
+It will ask to restart apache, showing the command to run which is::
+
+    # systemctl restart apache2
+
+Finally, to activate the site, type::
+
+    # a2ensite stouyapi
+
+If you used another name for the site configuration file, change the name in the above command.
+If you just type a2ensite and press enter it will show you all the sites available to activate
+and you just type the name of the site and press enter.
+
+Finally, it will ask to reload apache, which we will do with the command::
+
+    # systemctl reload apache2
+
+With that we finish the settings and the site is already running.
+
+To check if everything is ok, in the terminal::
+
+    ##To check if normal API routes work, type:
+    $ curl -I http://stouyapi.cwboo/api/firmware_builds
+
+    ##To check if rewritten API routes work, type:
+    $ curl -I http://stouyapi.cwboo/api/v1/discover/discover
+
+    ##To check if PHP routes work, type:
+    $ curl -I http://stouyapi.cwboo/api/v1/gamers/me
+
+All curl commands above should return ``HTTP/1.1 200 OK`` with some other information.
+
+
+5 - Configuring the files in the OUYA
+=====================================
+
+We must access the OUYA through adb, either in the case of an installation after a factory reset
+or to use the local stouyapi, and edit the hosts file located in /etc (/etc/hosts) and include a
+line with the format below::
+
+    IP-APACHE-SERVER STOUYAPI-SITE-NAME
+
+It will look like this::
+
+    127.0.0.1 localhost
+    192.168.0.5 stouyapi.local
+
+ATTENTION: The hosts file already has a line that refers to localhost and it should not be deleted.
+Also, you must leave a blank line after your stouyapi address.
+
+And the ouya_config.properties file, which is in /sdcard, will look like this::
+
+    OUYA_SERVER_URL=http://stouyapi.local
+    OUYA_STATUS_SERVER_URL=http://stouyapi.local/api/v1/status
+
+ATTENTION: the site to be used, which in the above case is stouyapi.local, is the one that we inform
+in the apache configuration file, in the line that starts with "ServerName".
+
+With this, the OUYA will use the local stouyapi immediately.
+If it do not, reboot the OUYA once.
+
+
+6 - OUYA setup
+==============
+
+1. User registration: "Existing account"
+2. Enter any username, leave password empty. Continue.
+3. Skip credit card registration
+
+The username will appear on your ouya main screen.
index 0b7b800f4a3062d4bbcd6289c7bdce1b1f64df35..dae49f3515b2db574afa051c128e0f955718dfd7 100644 (file)
@@ -16,8 +16,8 @@ OUYA config change
 - Create file ``ouya_config.properties``
 - Add::
 
-    OUYA_SERVER_URL=http://stouyapi.boo
-    OUYA_STATUS_SERVER_URL=http://stouyapi.boo/api/v1/status
+    OUYA_SERVER_URL=http://stouyapi.example.org
+    OUYA_STATUS_SERVER_URL=http://stouyapi.example.org/api/v1/status
 
 The changes should take effect immediately.
 If they do not, reboot the OUYA once.
@@ -30,19 +30,65 @@ OUYA setup
 2. Enter any username, leave password empty. Continue.
 3. Skip credit card registration
 
+The username will appear on your ouya main screen.
+
 
 Apache setup
 ============
+
+.. note:: Step-by-step setup instructions can be found in
+          `HOWTO-SETUP.rst <HOWTO-SETUP.rst>`__.
+
+
 Virtual host configuration::
 
-  Script PUT /empty-json.php
-  Script DELETE /api/v1/queued_downloads_delete.php
+  <VirtualHost *:80>
+    ServerName stouyapi.example.org
+    DocumentRoot /path/to/stouyapi/www
+
+    CustomLog /var/log/apache2/stouyapi-access.log combined
+    ErrorLog  /var/log/apache2/stouyapi-error.log
+
+    Script PUT /empty-json.php
+    Script DELETE /api/v1/queued_downloads_delete.php
+
+    <Directory "/path/to/stouyapi/www">
+      AllowOverride All
+      Require all granted
+    </Directory>
+  </VirtualHost>
 
-``mod_actions`` need to be enabled for apache 2.4.
+The following modules need to be enabled in Apache 2.4
+(with e.g. ``a2enmod``):
+
+- ``actions``
+- ``expires``
+- ``php`` (or php-fpm via fastcgi)
+- ``rewrite``
 
 The virtual host's document root needs to point to the ``www`` folder.
 
 
+Test your Apache setup
+----------------------
+::
+
+   # check if normal API routes work
+   $ curl -I http://stouyapi.example.org/api/firmware_builds
+   HTTP/1.1 200 OK
+   [...]
+
+   # check if rewritten API routes work
+   $ curl -I http://stouyapi.example.org/api/v1/discover/discover
+   HTTP/1.1 200 OK
+   [...]
+
+   # check if PHP routes work
+   curl -I http://stouyapi.example.org/api/v1/gamers/me
+   HTTP/1.1 200 OK
+   [...]
+
+
 Building API data
 =================
 Download the OUYA game data repository from
index c00f811cef8a6ebbb65575d29830375a6ba6bba5..5f601bf57cb892fd736e68111255bc84ee3c419c 100755 (executable)
@@ -31,7 +31,7 @@ foreach (glob($gameDetailsDir . '*.json') as $gameDataFile) {
     $htmlFile = basename($gameDataFile, '.json') . '.htm';
     file_put_contents(
         $wwwGameDir . $htmlFile,
-        renderGameFile($gameDataFile)
+        renderGameFile($gameDataFile, 'game/' . $htmlFile)
     );
 }
 
@@ -42,17 +42,17 @@ foreach (glob($discoverDir . '*.json') as $discoverFile) {
     }
     file_put_contents(
         $wwwDiscoverDir . $htmlFile,
-        renderDiscoverFile($discoverFile)
+        renderDiscoverFile($discoverFile, 'discover/' . $htmlFile)
     );
 }
 
 file_put_contents(
     $wwwDiscoverDir . 'allgames.htm',
-    renderAllGamesList(glob($gameDetailsDir . '*.json'))
+    renderAllGamesList(glob($gameDetailsDir . '*.json'), 'discover/allgames.htm')
 );
 
 
-function renderAllGamesList($detailsFiles)
+function renderAllGamesList($detailsFiles, $path)
 {
     $games = [];
     foreach ($detailsFiles as $gameDataFile) {
@@ -77,6 +77,7 @@ function renderAllGamesList($detailsFiles)
     $navLinks = [
         './' => 'back',
     ];
+    $canonicalUrl = $GLOBALS['baseUrl'] . $path;
 
     $allGamesTemplate = __DIR__ . '/../data/templates/allgames.tpl.php';
     ob_start();
@@ -87,11 +88,13 @@ function renderAllGamesList($detailsFiles)
     return $html;
 }
 
-function renderDiscoverFile($discoverFile)
+function renderDiscoverFile($discoverFile, $path)
 {
     $json = json_decode(file_get_contents($discoverFile));
 
     $title    = $json->title . ' OUYA games';
+    $subtitle = $json->stouyapi->subtitle ?? null;
+
     $sections = [];
     foreach ($json->rows as $row) {
         $section = (object) [
@@ -138,6 +141,11 @@ function renderDiscoverFile($discoverFile)
         $navLinks['./'] = 'discover';
     }
 
+    if ($path === 'discover/index.htm') {
+        $path = 'discover/';
+    }
+    $canonicalUrl = $GLOBALS['baseUrl'] . $path;
+
     $discoverTemplate = __DIR__ . '/../data/templates/discover.tpl.php';
     ob_start();
     include $discoverTemplate;
@@ -147,7 +155,7 @@ function renderDiscoverFile($discoverFile)
     return $html;
 }
 
-function renderGameFile($gameDataFile)
+function renderGameFile($gameDataFile, $path)
 {
     $json = json_decode(file_get_contents($gameDataFile));
 
@@ -167,9 +175,18 @@ function renderGameFile($gameDataFile)
         $apkDownloadUrl = null;
     }
     */
+    $developerDetailsUrl = null;
+    if (isset($json->developer->url) && $json->developer->url) {
+        $developerDetailsUrl = '../discover/' . str_replace(
+            'ouya://launcher/discover/',
+            '',
+            $json->developer->url
+        ) . '.htm';
+    }
 
     $internetArchiveUrl = $json->stouyapi->{'internet-archive'} ?? null;
     $developerUrl       = $json->stouyapi->{'developer-url'} ?? null;
+    $canonicalUrl       = $GLOBALS['baseUrl'] . $path;
 
     $pushUrl = $GLOBALS['pushToMyOuyaUrl']
         . '?game=' . urlencode($json->apk->package);
diff --git a/bin/clean-generated-and-qr.sh b/bin/clean-generated-and-qr.sh
new file mode 100755 (executable)
index 0000000..2c8f9df
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/sh
+# remove all autogenerated files, including qr
+set -e
+./bin/clean-generated-keep-qr.sh
+test -d www/gen-qr/ && rm -f -r www/gen-qr/
diff --git a/bin/clean-generated-keep-qr.sh b/bin/clean-generated-keep-qr.sh
new file mode 100755 (executable)
index 0000000..d5a3840
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/sh
+# remove all autogenerated files except qr codes
+# QR codes are expensive to generate
+set -e
+rm -f www/api/v1/apps/*
+rm -f www/api/v1/details-data/*
+rm -f -r www/api/v1/developers/*
+rm -f www/api/v1/discover-data/*
+rm -f www/api/v1/games/*/*
+rmdir  www/api/v1/games/*/ || true
+rm -f www/api/v1/search-data/*
+rm -f www/discover/*
+rm -f www/game/*
+rm -f www/game-data-version
diff --git a/bin/clean-generated.sh b/bin/clean-generated.sh
deleted file mode 100755 (executable)
index c6a6421..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-# remove all autogenerated files
-rm www/api/v1/apps/*
-rm www/api/v1/details-data/*
-rm -r www/api/v1/developers/*
-rm www/api/v1/discover-data/*
-rm www/api/v1/games/*/*
-rm www/api/v1/search-data/*
-rm www/discover/*
-rm www/game/*
diff --git a/bin/create-qr.sh b/bin/create-qr.sh
new file mode 100755 (executable)
index 0000000..c344b72
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/sh
+# Create an image that contains a QR code for the given website
+# together with the URL as readable text
+set -e
+
+if [ "$#" -lt 2 ]; then
+    echo "Usage: create-qr.sh https://example.org outfile.png" > /dev/stderr
+    exit 1
+fi
+
+if ! command -v convert > /dev/null; then
+    echo "convert (imagemagick) is not installed" > /dev/stderr
+    exit 2
+fi
+
+if ! command -v exiftool > /dev/null; then
+    echo "exiftool is not installed" > /dev/stderr
+    exit 2
+fi
+
+if ! command -v qrencode > /dev/null; then
+    echo "qrencode is not installed" > /dev/stderr
+    exit 2
+fi
+
+url="$1"
+filename="$2"
+
+qrencode -s 20 -o tmp-qr.png "$url"
+
+convert\
+    -filter point -resize 1260x580\
+    -background '#1a1a1a'\
+    tmp-qr.png\
+    -size 1260x120\
+    -fill '#dad9d9'\
+    -gravity south\
+    label:"$url"\
+    -append\
+    -bordercolor '#1a1a1a'\
+    -border 10\
+    "$filename"
+
+rm tmp-qr.png
+
+exiftool\
+    -quiet\
+    -ignoreMinorErrors\
+    -overwrite_original\
+    -PNG:Software=stouyapi\
+    -PNG:Title="$url"\
+    "$filename"
index fc16ef3636569802790a20ecb56b986a58543b47..b225c9bd8f90fa8b2d1b81492d1481c186de7327 100644 (file)
@@ -31,7 +31,7 @@ function filterByLetter($origGames, $letter)
 {
     $filtered = [];
     foreach ($origGames as $game) {
-        $gameLetter = strtoupper($game->title{0});
+        $gameLetter = strtoupper($game->title[0]);
         if (!preg_match('#^[A-Z]$#', $gameLetter)) {
             $gameLetter = 'Other';
         }
@@ -78,6 +78,19 @@ function filterBySearchWord($origGames, $searchWord)
     return $filtered;
 }
 
+function filterLastAdded($origGames, $limit)
+{
+    $games = array_values($origGames);
+    usort(
+        $games,
+        function ($gameA, $gameB) {
+            return strtotime($gameB->firstRelease->date) - strtotime($gameA->firstRelease->date);
+        }
+    );
+
+    return array_slice($games, 0, $limit);
+}
+
 function filterLastUpdated($origGames, $limit)
 {
     $games = array_values($origGames);
@@ -104,6 +117,14 @@ function filterBestRated($origGames, $limit)
     return array_slice($games, 0, $limit);
 }
 
+function filterBestRatedGames($origGames, $limit)
+{
+    $noApps = filterByGenre($origGames, 'App', true);
+    $noAppsNoEmus = filterByGenre($noApps, 'Emulator', true);
+
+    return filterBestRated($noAppsNoEmus, $limit);
+}
+
 function filterMostDownloaded($origGames, $limit)
 {
     $games = array_values($origGames);
index fe94108bbc41b72e80fb7ea50b7ccef74d3509c9..bf086f4483f7470ce33baf35bf9860288ae3c72c 100644 (file)
@@ -5,6 +5,10 @@
 
 function categoryPath($title)
 {
-    return str_replace(['/', '\\', ' ', '+', '?'], '_', $title);
+    if ($title == 'Tutorials') {
+        //OUYAs fetch "Make" from "discover/tutorials"
+        $title = strtolower($title);
+    }
+    return str_replace(['/', '\\', ' ', '+', '?', '!'], '_', $title);
 }
 ?>
index 182708a050e0337f3f3c5c06b4117efa7430ddbd..854b3d6762c6302a64c1c7ef4502d699fc499fdb 100755 (executable)
@@ -9,15 +9,36 @@
 ini_set('xdebug.halt_level', E_WARNING|E_NOTICE|E_USER_WARNING|E_USER_NOTICE);
 require_once __DIR__ . '/functions.php';
 require_once __DIR__ . '/filters.php';
-if (!isset($argv[1])) {
+
+//command line option parsing
+$optind = null;
+$opts = getopt('h', ['help', 'mini', 'noqr'], $optind);
+$args = array_slice($argv, $optind);
+
+if (isset($opts['help']) || isset($opts['h'])) {
+    echo "Import games from a OUYA game data repository\n";
+    echo "\n";
+    echo "Usage: import-game-data.php [--mini] [--noqr] [--help|-h]\n";
+    echo " --mini  Generate small but ugly JSON files\n";
+    echo " --noqr  Do not generate and link QR code images\n";
+    exit(0);
+}
+
+if (!isset($args[0])) {
     error('Pass the path to a "folders" file with game data json files folder names');
 }
-$foldersFile = $argv[1];
+$foldersFile = $args[0];
 if (!is_file($foldersFile)) {
     error('Given path is not a file: ' . $foldersFile);
 }
 
+$cfgMini     = isset($opts['mini']);
+$cfgEnableQr = !isset($opts['noqr']);
+
+
 //default configuration values
+$GLOBALS['baseUrl']      = 'http://ouya.cweiske.de/';
+$GLOBALS['categorySubtitles'] = [];
 $GLOBALS['packagelists'] = [];
 $GLOBALS['urlRewrites']  = [];
 $cfgFile = __DIR__ . '/../config.php';
@@ -27,6 +48,13 @@ if (file_exists($cfgFile)) {
 
 $wwwDir = __DIR__ . '/../www/';
 
+if ($cfgEnableQr) {
+    $qrDir = $wwwDir . 'gen-qr/';
+    if (!is_dir($qrDir)) {
+        mkdir($qrDir, 0775);
+    }
+}
+
 $baseDir   = dirname($foldersFile);
 $gameFiles = [];
 foreach (file($foldersFile) as $line) {
@@ -43,9 +71,18 @@ foreach (file($foldersFile) as $line) {
     }
 }
 
+//store git repository version of last folder
+$workdir = getcwd();
+chdir($folder);
+$gitDate = `git log --max-count=1 --format="%h %cI"`;
+chdir($workdir);
+file_put_contents($wwwDir . '/game-data-version', $gitDate);
+
 $games = [];
 $count = 0;
 $developers = [];
+
+//load game data. doing early to collect a developer's games
 foreach ($gameFiles as $gameFile) {
     $game = json_decode(file_get_contents($gameFile));
     if ($game === null) {
@@ -54,18 +91,18 @@ foreach ($gameFiles as $gameFile) {
     addMissingGameProperties($game);
     $games[$game->packageName] = $game;
 
-    writeJson(
-        'api/v1/details-data/' . $game->packageName . '.json',
-        buildDetails($game)
-    );
-
     if (!isset($developers[$game->developer->uuid])) {
         $developers[$game->developer->uuid] = [
-            'info'     => $game->developer,
-            'products' => [],
+            'info'      => $game->developer,
+            'products'  => [],
+            'gameNames' => [],
         ];
     }
+    $developers[$game->developer->uuid]['gameNames'][] = $game->packageName;
+}
 
+//write json api files
+foreach ($games as $game) {
     $products = $game->products ?? [];
     foreach ($products as $product) {
         writeJson(
@@ -76,7 +113,14 @@ foreach ($gameFiles as $gameFile) {
         $developers[$game->developer->uuid]['products'][] = $product;
     }
 
-    /**/
+    writeJson(
+        'api/v1/details-data/' . $game->packageName . '.json',
+        buildDetails(
+            $game,
+            count($developers[$game->developer->uuid]['gameNames']) > 1
+        )
+    );
+
     writeJson(
         'api/v1/games/' . $game->packageName . '/purchases',
         buildPurchases($game)
@@ -112,14 +156,25 @@ foreach ($developers as $developer) {
         buildDeveloperProducts($developer['products'], $developer['info'])
     );
     writeJson(
-        //index.htm does not need a rewrite rule
         'api/v1/developers/' . $developer['info']->uuid
         . '/current_gamer',
         buildDeveloperCurrentGamer()
     );
+
+    if (count($developer['gameNames']) > 1) {
+        writeJson(
+            'api/v1/discover-data/dev--' . $developer['info']->uuid . '.json',
+            buildSpecialCategory(
+                'Developer: ' . $developer['info']->name,
+                filterByPackageNames($games, $developer['gameNames'])
+            )
+        );
+    }
 }
 
-writeJson('api/v1/discover-data/discover.json', buildDiscover($games));
+$data = buildDiscover($games);
+writeJson('api/v1/discover-data/discover.json', $data);
+writeJson('api/v1/discover-data/discover.forge.json', convertCategoryToForge($data));
 writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games));
 
 //make
@@ -148,12 +203,13 @@ function buildDiscover(array $games)
     ];
 
     addDiscoverRow(
-        $data, 'Last Updated',
-        filterLastUpdated($games, 10)
+        $data, 'New games',
+        filterLastAdded($games, 10)
     );
     addDiscoverRow(
-        $data, 'Best rated',
-        filterBestRated($games, 10)
+        $data, 'Best rated games',
+        filterBestRatedGames($games, 10),
+        true
     );
 
     foreach ($GLOBALS['packagelists'] as $listTitle => $listPackageNames) {
@@ -167,25 +223,35 @@ function buildDiscover(array $games)
         $data, 'Special',
         [
             'Best rated',
+            'Best rated games',
             'Most rated',
             'Random',
+            'Last updated',
         ]
     );
-    writeJson(
+    writeCategoryJson(
         'api/v1/discover-data/' . categoryPath('Best rated') . '.json',
         buildSpecialCategory('Best rated', filterBestRated($games, 99))
     );
-    writeJson(
+    writeCategoryJson(
+        'api/v1/discover-data/' . categoryPath('Best rated games') . '.json',
+        buildSpecialCategory('Best rated games', filterBestRatedGames($games, 99))
+    );
+    writeCategoryJson(
         'api/v1/discover-data/' . categoryPath('Most rated') . '.json',
         buildSpecialCategory('Most rated', filterMostDownloaded($games, 99))
     );
-    writeJson(
+    writeCategoryJson(
         'api/v1/discover-data/' . categoryPath('Random') . '.json',
         buildSpecialCategory(
             'Random ' . date('Y-m-d H:i'),
             filterRandom($games, 99)
         )
     );
+    writeCategoryJson(
+        'api/v1/discover-data/' . categoryPath('Last updated') . '.json',
+        buildSpecialCategory('Last updated', filterLastUpdated($games, 99))
+    );
 
     $players = [
         //1 => '1 player',
@@ -195,9 +261,20 @@ function buildDiscover(array $games)
     ];
     addDiscoverRow($data, 'Multiplayer', $players);
     foreach ($players as $num => $title) {
-        writeJson(
+        writeCategoryJson(
             'api/v1/discover-data/' . categoryPath($title) . '.json',
-            buildDiscoverCategory($title, filterByPlayers($games, $num))
+            buildDiscoverCategory(
+                $title,
+                //I do not want emulators here,
+                // and neither Streaming apps
+                filterByGenre(
+                    filterByGenre(
+                        filterByPlayers($games, $num),
+                        'Emulator', true
+                    ),
+                    'App', true
+                )
+            )
         );
     }
 
@@ -205,7 +282,7 @@ function buildDiscover(array $games)
     natsort($ages);
     addDiscoverRow($data, 'Content rating', $ages);
     foreach ($ages as $num => $title) {
-        writeJson(
+        writeCategoryJson(
             'api/v1/discover-data/' . categoryPath($title) . '.json',
             buildDiscoverCategory($title, filterByAge($games, $title))
         );
@@ -216,7 +293,7 @@ function buildDiscover(array $games)
     addChunkedDiscoverRows($data, $genres, 'Genres');
 
     foreach ($genres as $genre) {
-        writeJson(
+        writeCategoryJson(
             'api/v1/discover-data/' . categoryPath($genre) . '.json',
             buildDiscoverCategory($genre, filterByGenre($games, $genre))
         );
@@ -225,7 +302,7 @@ function buildDiscover(array $games)
     $abc = array_merge(range('A', 'Z'), ['Other']);
     addChunkedDiscoverRows($data, $abc, 'Alphabetical');
     foreach ($abc as $letter) {
-        writeJson(
+        writeCategoryJson(
             'api/v1/discover-data/' . categoryPath($letter) . '.json',
             buildDiscoverCategory($letter, filterByLetter($games, $letter))
         );
@@ -244,24 +321,72 @@ function buildDiscoverCategory($name, $games)
         'rows'  => [],
         'tiles' => [],
     ];
-    addDiscoverRow(
-        $data, 'Last Updated',
-        filterLastUpdated($games, 10)
-    );
-    addDiscoverRow(
-        $data, 'Best rated',
-        filterBestRated($games, 10)
-    );
+    if (isset($GLOBALS['categorySubtitles'][$name])) {
+        $data['stouyapi']['subtitle'] = $GLOBALS['categorySubtitles'][$name];
+    }
+
+    if (count($games) >= 20) {
+        addDiscoverRow(
+            $data, 'Last updated',
+            filterLastUpdated($games, 10)
+        );
+        addDiscoverRow(
+            $data, 'Best rated',
+            filterBestRated($games, 10),
+            true
+        );
+    }
 
     $games = sortByTitle($games);
     $chunks = array_chunk($games, 4);
+    $title = 'All';
     foreach ($chunks as $chunkGames) {
-        addDiscoverRow($data, '', $chunkGames);
+        addDiscoverRow($data, $title, $chunkGames);
+        $title = '';
     }
 
     return $data;
 }
 
+/**
+ * Modify a category to make it suitable for the Razer Forge TV
+ *
+ * - Fold rows without title into the previous row
+ * - Remove automatically generated categories ("Last updated", "Best rated")
+ *
+ * @see buildDiscoverCategory()
+ */
+function convertCategoryToForge($data, $removeAutoCategories = false)
+{
+    //merge tiles from rows without title into the previous row
+    $lastTitleRowId = null;
+    foreach ($data['rows'] as $rowId => $row) {
+        if ($row['title'] !== '') {
+            $lastTitleRowId = $rowId;
+        } else if ($lastTitleRowId !== null) {
+            $data['rows'][$lastTitleRowId]['tiles'] = array_merge(
+                $data['rows'][$lastTitleRowId]['tiles'],
+                $row['tiles']
+            );
+            unset($data['rows'][$rowId]);
+        }
+    }
+
+    if ($removeAutoCategories) {
+        foreach ($data['rows'] as $rowId => $row) {
+            if ($row['title'] === 'Last updated'
+                || $row['title'] === 'Best rated'
+            ) {
+                unset($data['rows'][$rowId]);
+            }
+        }
+    }
+
+    $data['rows'] = array_values($data['rows']);
+
+    return $data;
+}
+
 function buildMakeCategory($name, $games)
 {
     $data = [
@@ -276,6 +401,11 @@ function buildMakeCategory($name, $games)
     return $data;
 }
 
+/**
+ * Category without the "Last updated" or "Best rated" top rows
+ *
+ * Used for "Best rated", "Most rated", "Random"
+ */
 function buildSpecialCategory($name, $games)
 {
     $data = [
@@ -297,19 +427,29 @@ function buildSpecialCategory($name, $games)
 
 function buildDiscoverHome(array $games)
 {
-    //we do not want anything here for now
     $data = [
         'title' => 'home',
         'rows'  => [
-            [
-                'title' => 'FEATURED',
-                'showPrice' => false,
-                'ranked'    => false,
-                'tiles'     => [],
-            ]
         ],
         'tiles' => [],
     ];
+
+    if (isset($GLOBALS['home'])) {
+        reset($GLOBALS['home']);
+        $title = key($GLOBALS['home']);
+        addDiscoverRow(
+            $data, $title,
+            filterByPackageNames($games, $GLOBALS['home'][$title])
+        );
+    } else {
+        $data['rows'][] = [
+            'title'     => 'FEATURED',
+            'showPrice' => false,
+            'ranked'    => false,
+            'tiles'     => [],
+        ];
+    }
+
     return $data;
 }
 
@@ -375,7 +515,7 @@ function buildAppDownload($game, $release)
             'fileSize'      => $release->size,
             'version'       => $release->uuid,
             'contentRating' => $game->contentRating,
-            'downloadLink'  => rewriteUrl($release->url),
+            'downloadLink'  => $release->url,
         ]
     ];
 }
@@ -401,7 +541,7 @@ function buildProduct($product)
 /**
  * Build /app/v1/details?app=org.example.game
  */
-function buildDetails($game)
+function buildDetails($game, $linkDeveloperPage = false)
 {
     $latestRelease = $game->latestRelease;
 
@@ -456,11 +596,18 @@ function buildDetails($game)
         $iaUrl = dirname($game->latestRelease->url) . '/';
     }
 
+    $description = $game->description;
+    if (isset($game->notes) && trim($game->notes)) {
+        $description = "Technical notes:\r\n" . $game->notes
+            . "\r\n----\r\n"
+            . $description;
+    }
+
     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
-    return [
+    $data = [
         'type'             => 'Game',
         'title'            => $game->title,
-        'description'      => $game->description,
+        'description'      => $description,
         'gamerNumbers'     => $game->players,
         'genres'           => $game->genres,
 
@@ -520,6 +667,13 @@ function buildDetails($game)
             'developer-url'    => $game->developer->website ?? null,
         ]
     ];
+
+    if ($linkDeveloperPage) {
+        $data['developer']['url'] = 'ouya://launcher/discover/dev--'
+            . categoryPath($game->developer->uuid);
+    }
+
+    return $data;
 }
 
 function buildDeveloperCurrentGamer()
@@ -630,12 +784,12 @@ function addChunkedDiscoverRows(&$data, $games, $title)
     }
 }
 
-function addDiscoverRow(&$data, $title, $games)
+function addDiscoverRow(&$data, $title, $games, $ranked = false)
 {
     $row = [
         'title'     => $title,
         'showPrice' => true,
-        'ranked'    => false,
+        'ranked'    => $ranked,
         'tiles'     => [],
     ];
     foreach ($games as $game) {
@@ -734,6 +888,8 @@ function getAllGenres($games)
 
 function addMissingGameProperties($game)
 {
+    global $cfgEnableQr;
+
     if (!isset($game->overview)) {
         $game->overview = null;
     }
@@ -772,9 +928,14 @@ function addMissingGameProperties($game)
         $game->rating->count = 0;
     }
 
+    $game->firstRelease  = null;
     $game->latestRelease = null;
+    $firstReleaseTimestamp  = null;
     $latestReleaseTimestamp = 0;
     foreach ($game->releases as $release) {
+        if (isset($release->broken) && $release->broken) {
+            continue;
+        }
         if (!isset($release->publicSize)) {
             $release->publicSize = 0;
         }
@@ -787,6 +948,15 @@ function addMissingGameProperties($game)
             $game->latestRelease    = $release;
             $latestReleaseTimestamp = $releaseTimestamp;
         }
+        if ($firstReleaseTimestamp === null
+            || $releaseTimestamp < $firstReleaseTimestamp
+        ) {
+            $game->firstRelease    = $release;
+            $firstReleaseTimestamp = $releaseTimestamp;
+        }
+    }
+    if ($game->firstRelease === null) {
+        error('No first release for ' . $game->packageName);
     }
     if ($game->latestRelease === null) {
         error('No latest release for ' . $game->packageName);
@@ -811,6 +981,34 @@ function addMissingGameProperties($game)
     if (!isset($game->developer->founder)) {
         $game->developer->founder = false;
     }
+
+    if ($cfgEnableQr && $game->website) {
+        $qrfileName = preg_replace('#[^\\w\\d._-]#', '_', $game->website) . '.png';
+        $qrfilePath = $GLOBALS['qrDir'] . $qrfileName;
+        if (!file_exists($qrfilePath)) {
+            $cmd = __DIR__ . '/create-qr.sh'
+                 . ' ' . escapeshellarg($game->website)
+                 . ' ' . escapeshellarg($qrfilePath);
+            passthru($cmd, $retval);
+            if ($retval != 0) {
+                exit(20);
+            }
+        }
+        $qrUrlPath = $GLOBALS['baseUrl'] . 'gen-qr/' . $qrfileName;
+        $game->media[] = (object) [
+            'type' => 'image',
+            'url'  => $qrUrlPath,
+        ];
+    }
+
+    //rewrite urls from Internet Archive to our servers
+    $game->discover = rewriteUrl($game->discover);
+    foreach ($game->media as $medium) {
+        $medium->url = rewriteUrl($medium->url);
+    }
+    foreach ($game->releases as $release) {
+        $release->url = rewriteUrl($release->url);
+    }
 }
 
 /**
@@ -906,18 +1104,31 @@ function rewriteUrl($url)
 
 function writeJson($path, $data)
 {
-    global $wwwDir;
+    global $cfgMini, $wwwDir;
     $fullPath = $wwwDir . $path;
     $dir = dirname($fullPath);
     if (!is_dir($dir)) {
         mkdir($dir, 0777, true);
     }
+    $opts = JSON_UNESCAPED_SLASHES;
+    if (!$cfgMini) {
+        $opts |= JSON_PRETTY_PRINT;
+    }
     file_put_contents(
         $fullPath,
-        json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
+        json_encode($data, $opts) . "\n"
     );
 }
 
+function writeCategoryJson($path, $data)
+{
+    writeJson($path, $data);
+
+    $forgePath = str_replace('.json', '.forge.json', $path);
+    $forgeData = convertCategoryToForge($data, true);
+    writeJson($forgePath, $forgeData);
+}
+
 function error($msg)
 {
     fwrite(STDERR, $msg . "\n");
index 5087c2210c8657dd7813282da41ad50cd0113b7b..31fad965ff1fec3bff705db869c5346ff2a50bb1 100644 (file)
@@ -3,6 +3,8 @@
  * Optional configuration file for import-game-data.php
  * Copy it to config.php and adjust it.
  */
+$GLOBALS['baseUrl'] = 'http://ouya.cweiske.de/';
+
 $GLOBALS['urlRewrites'] = [
     '#^https://archive.org/download/#' => 'http://statics.ouya.world/ia/',
 ];
@@ -11,4 +13,27 @@ $GLOBALS['packagelists']["cweiske's picks"] = [
     'com.cosmos.babyloniantwins',
     'com.inverseblue.skyriders',
 ];
+$GLOBALS['home']['2020 Winter GameJam'] = [
+    'com.DESKINK.ToneTests',
+    'com.Eightbbgames.yahayor',
+    'com.FuzzyPopcorn',
+    'com.NYYLE.NYCTO',
+    'com.NoelRojasOliveras.PaintKiller',
+    'com.StrawHat.Fall',
+    'com.oliverstogden.trf',
+    'com.samuelsousa.shootdestroy',
+    'com.scorpion.shootout',
+    'com.sd_game_dev.aliens_taste_my_sword',
+    'com.sumotown.sirtet',
+    'de.x2041.games.gyrogun',
+    'ht.sr.git.arcticGrind.embed',
+    'tv.ouya.demo.DarkSpacePioneer',
+    'tv.ouya.win.unity.brokenbeauty',
+];
 $GLOBALS['pushToMyOuyaUrl'] = '../push-to-my-ouya.php';
+
+$GLOBALS['categorySubtitles'] = [
+    'Exclusive' => 'Only available on the OUYA',
+    'Unlocked' => 'Official OUYA shutdown releases',
+    'VIP Room' => 'Once exclusive on the OUYA',
+];
index ded0ebd153f1b285794c324189f68b9345e1d855..1685a37e54cdfeb7f21fbc2a4d8d7591ed11c02e 100644 (file)
@@ -8,11 +8,12 @@
   <link rel="stylesheet" type="text/css" href="../datatables/jquery.dataTables.yadcf.css"/>
   <link rel="stylesheet" type="text/css" href="../ouya-allgames.css"/>
   <link rel="icon" href="../favicon.ico"/>
+  <link rel="canonical" href="<?= htmlspecialchars($canonicalUrl) ?>"/>
  </head>
  <body class="allgames">
   <header>
    <h1>List of all OUYA games</h1>
-   <img class="ouyalogo" src="../ouya-logo.grey.svg" alt="OUYA logo" width="50"/>
+   <a href="./"><img class="ouyalogo" src="../ouya-logo.grey.svg" alt="OUYA logo" width="50"/></a>
   </header>
 
   <table id="allouyagames" class="display">
index cd078ddb5a25a03b77c5424df5efb399ebb4e711..e422a05df589d9a2860f43d8ebfa1418d55f794d 100644 (file)
@@ -6,11 +6,12 @@
   <meta name="generator" content="stouyapi"/>
   <link rel="stylesheet" type="text/css" href="../ouya-discover.css"/>
   <link rel="icon" href="../favicon.ico"/>
+  <link rel="canonical" href="<?= htmlspecialchars($canonicalUrl) ?>"/>
  </head>
  <body class="discover">
   <header>
-   <h1><?= htmlspecialchars($json->title); ?></h1>
-   <img class="ouyalogo" src="../ouya-logo.grey.svg" alt="OUYA logo" width="50"/>
+   <h1><?= htmlspecialchars($json->title); ?><?php if($subtitle) { echo ': ' . $subtitle; } ?></h1>
+   <a href="./"><img class="ouyalogo" src="../ouya-logo.grey.svg" alt="OUYA logo" width="50"/></a>
   </header>
 
   <?php foreach ($sections as $section): ?>
@@ -24,7 +25,7 @@
     <section class="tile<?= ($tile->thumb == '') ? ' noimg' : '' ?>">
      <h3 class="title"><a href="<?= htmlspecialchars($tile->detailUrl) ?>"><?= htmlspecialchars($tile->title) ?></a></h3>
      <?php if ($tile->thumb != ''): ?>
-     <a href="<?= htmlspecialchars($tile->detailUrl) ?>"><img src="<?= htmlspecialchars($tile->thumb) ?>" alt="Screenshot of <?= htmlspecialchars($tile->detailUrl) ?>"/></a>
+     <a href="<?= htmlspecialchars($tile->detailUrl) ?>"><img src="<?= htmlspecialchars($tile->thumb) ?>" alt="Screenshot of <?= htmlspecialchars($tile->title) ?>" width="732" height="412"/></a>
      <?php endif ?>
      <?php if ($tile->type == 'app' && $tile->ratingCount > 0): ?>
      <p class="rating">
index 4f7779e2f4d13d0f6452440cb77683c53d1b3455..04b6317c6f8a56df3cc7724dd63f8630176278ab 100644 (file)
@@ -7,10 +7,15 @@
   <meta name="author" content="<?= htmlspecialchars($json->developer->name) ?>"/>
   <link rel="stylesheet" type="text/css" href="../ouya-game.css"/>
   <link rel="icon" href="../favicon.ico"/>
+  <link rel="canonical" href="<?= htmlspecialchars($canonicalUrl) ?>"/>
+  <meta name="twitter:card" content="summary_large_image"/>
+  <meta property="og:title" content="<?= htmlspecialchars($json->title); ?>" />
+  <meta property="og:description" content="<?= htmlspecialchars(substr(strtok($json->description, '.!'), 0, 200)); ?>." />
+  <meta property="og:image" content="<?= htmlspecialchars($json->tileImage); ?>" />
  </head>
  <body class="game">
   <header>
-   <img class="ouyalogo" src="../ouya-logo.grey.svg" alt="OUYA logo" width="50"/>
+   <a href="../discover/"><img class="ouyalogo" src="../ouya-logo.grey.svg" alt="OUYA logo" width="50"/></a>
   </header>
   <section class="text">
    <h1><?= htmlspecialchars($json->title); ?></h1>
     <a href="<?= $internetArchiveUrl ?>">Internet Archive</a>
    </div>
    <?php endif ?>
+   <?php if ($developerDetailsUrl): ?>
+   <div>
+    <a href="<?= $developerDetailsUrl ?>">Developer page</a>
+   </div>
+   <?php endif ?>
    <?php if ($appsJson->app->website): ?>
    <div>
     <a href="<?= $appsJson->app->website ?>">Game website</a>
index e12ed602306866a52509369e10a55e41172d79a6..8f2efe3a07ff0592b324ca8605211b9e822d81b7 100644 (file)
@@ -1,4 +1,18 @@
+#no iso-2022-cn charset for "com.cis.ouya.oth.htm"
+#"RemoveCharset" does not work, so we have to override with AddCharset
+AddCharset UTF-8 .iso2022-cn .cis
+#Fix game/art.var.pgo.htm
+AddHandler default-handler .var
+
+DirectorySlash Off
+
 RewriteEngine on
+RewriteBase /
+
+<Files "firmware_builds">
+    ExpiresActive On
+    ExpiresDefault "access plus 1 day"
+</Files>
 
 #rewrite details GET parameter
 RewriteCond %{QUERY_STRING} ^app=([^&]+)
@@ -16,9 +30,45 @@ RewriteCond %{QUERY_STRING} &only=([^&]+)
 RewriteCond %{DOCUMENT_ROOT}/api/v1/developers/$1/products/%1.json -f
 RewriteRule ^api/v1/developers/(.+)/products/ /api/v1/developers/$1/products/%1.json? [END]
 
+#discover homepage
+RewriteCond %{HTTP_USER_AGENT} "Forge"
+RewriteRule ^api/v1/discover/?$ /api/v1/discover-data/discover.forge.json [END]
+
 RewriteRule ^api/v1/discover/?$ /api/v1/discover-data/discover.json [END]
+
+#discover category
+RewriteCond %{HTTP_USER_AGENT} "Forge"
+RewriteCond %{REQUEST_URI} ^/api/v1/discover/(.+)$
+RewriteCond %{DOCUMENT_ROOT}/api/v1/discover-data/$1.forge.json -f
+RewriteRule ^api/v1/discover/(.+)$ /api/v1/discover-data/$1.forge.json [END]
+
 RewriteRule ^api/v1/discover/(.+)$ /api/v1/discover-data/$1.json [END]
 
+#if directoryslash is on, gamers gets redirected to gamers/ (dir index)
+# the ouya registration does not support redirects there
+#TODO: Use that only for the api/v1/gamers path, not for all
+ErrorDocument 400 /api/v1/gamers/register-error.json
+RewriteRule ^api/v1/gamers$ /api/v1/gamers/register-error.json [R=400,END]
+RewriteRule ^api/razer/gamer$ /api/v1/gamers/register-error.json [R=400,END]
+
+#prevent redirect from gamers/me to gamers/me/
+<Files "me">
+    DirectorySlash Off
+</Files>
+
+#Disable the next three lines to have static usernames only
+RewriteRule ^api/razer/session$ /api/razer/session.php [END]
+RewriteRule ^api/v1/gamers/me$ /api/v1/gamers/me.php [END]
+RewriteRule ^api/v1/sessions$ /api/v1/sessions.php [END]
+
+RewriteRule ^api/v1/gamers/me$ /api/v1/gamers/me.json [END]
+
+#partner builds
+RewriteCond %{HTTP_USER_AGENT} "razer/pearlyn/pearlyn"
+RewriteRule ^api/v1/partner_builds$ /api/v1/partner_builds.razer-forge-tv.json [END]
+
+RewriteRule ^api/v1/partner_builds$ /api/v1/partner_builds.json [END]
+
 #purchased games/products
 # active buy requests
 RewriteCond %{REQUEST_METHOD} POST
@@ -37,6 +87,9 @@ RewriteCond %{QUERY_STRING} &q=([^&]+)
 RewriteCond %{DOCUMENT_ROOT}/api/v1/search-data/%1.json -f
 RewriteRule ^api/v1/search /api/v1/search-data/%1.json? [END]
 
+# for http://clients3.google.com/generate_204
+RewriteRule ^generate_204$ /api/v1/status
+
 #this one wants a 204 status code
 RewriteRule ^api/v1/status$ - [R=204,L]
 
diff --git a/www/agreements/marketplace.html b/www/agreements/marketplace.html
new file mode 100644 (file)
index 0000000..93b9ef7
--- /dev/null
@@ -0,0 +1,16 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+  <title>stouyapi marketplace agreements</title>
+ </head>
+ <body>
+  <h1>stouyapi - OUYA API server</h1>
+  <p>
+   We store no data, so you do not need to agree to anything :)
+  </p>
+  <p>
+   Registration of new accounts does not work here!
+   Simply go back and choose "existing account",
+   type any username, leave the password empty and sign in.
+  </p>
+ </body>
+</html>
index 7b57bfdcc89f0288507007fa7d9a87e2eed6e862..cb68072eab7dfc8308af9dc4cc98147025c96ccc 100644 (file)
@@ -1,5 +1,5 @@
 {
-    "result": [        
+    "result": [
         {
             "incremental": false,
             "filename": "OUYA-1.2.1427-r1.zip",
             "requiredUpto": "1.2.995",
             "changes": null,
             "changesLocalized": {
-                "de": "no info",
+                "de": "v1.2.1427 \"Chickcharney Hotfix 2\" wurde am 2014-12-15 veröffentlich und ist die letzte offiziell erschienene Firmware.",
                 "it": "no info",
                 "fr": "no info",
                 "es": "no info",
-                "en": "no info"
+                "en": "v1.2.1427 \"Chickcharney Hotfix 2\" was released on 2014-12-15 and is the last official firmware that was ever published."
             }
         }
     ]
diff --git a/www/api/razer/session b/www/api/razer/session
new file mode 100644 (file)
index 0000000..3454a4d
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "token": "00702342-0000-1111-2222-c3e1500cafe1"
+}
diff --git a/www/api/razer/session.php b/www/api/razer/session.php
new file mode 100644 (file)
index 0000000..e32915b
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Store the desired username during the login process
+ *
+ * It will be read by the Razer Forge TV when calling api/v1/gamers/me.
+ *
+ * @author Christian Weiske <cweiske@cweiske.de>
+ * @see    api/razer/session
+ * @see    api/v1/gamers/me
+ */
+
+if (!isset($_POST['email'])) {
+    header('HTTP/1.0 400 Bad Request');
+    header('Content-type: application/json');
+    echo '{"error":{"message":"E-Mail missing","code": 2001}}' . "\n";
+    exit(1);
+}
+$email = $_POST['email'];
+
+//we use the ouya username storage code here
+// and simply use the part before the @ in the e-mail as username.
+list($_POST['username']) = explode('@', $email);
+require __DIR__ . '/../v1/sessions.php';
index 15d1661eb564cb10219823336883ec9c737c801a..8270891fc15c55828547806e4e13255a6263682c 100644 (file)
@@ -1,3 +1,4 @@
 {
-    "BTC_LAUNCHER_PACKAGES":"tv.ouya.xbmc,tunein.player,tv.twitch.android.viewer,com.plexapp.android,tv.ouya.bbciplayer,tv.ouya.hulu,tv.ouya.minecrafttv,tv.ouya.ouyabrowser,tv.ouya.pandora,tv.ouya.visiomgo,tv.ouya.youtube,tv.ouya.tubitv"
+    "BTC_LAUNCHER_PACKAGES":"tv.ouya.xbmc,tunein.player,tv.twitch.android.viewer,com.plexapp.android,tv.ouya.bbciplayer,tv.ouya.hulu,tv.ouya.minecrafttv,tv.ouya.ouyabrowser,tv.ouya.pandora,tv.ouya.visiomgo,tv.ouya.youtube,tv.ouya.tubitv",
+    "RATING_PROMPT_FREQ": 0
 }
diff --git a/www/api/v1/gamers/me.json b/www/api/v1/gamers/me.json
new file mode 100644 (file)
index 0000000..699922a
--- /dev/null
@@ -0,0 +1,11 @@
+{
+    "gamer": {
+        "uuid": "00702342-0000-1111-2222-c3e1500cafe2",
+        "settings": {},
+        "founder": false,
+        "email": "stouyapi@example.org",
+        "username": "stouyapi",
+        "nickname": "stouyapi",
+        "avatar": null
+    }
+}
diff --git a/www/api/v1/gamers/me.php b/www/api/v1/gamers/me.php
new file mode 100644 (file)
index 0000000..742e544
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Return user data with dynamic username that has been saved during login
+ *
+ * @author Christian Weiske <cweiske@cweiske.de>
+ * @see    api/v1/sessions
+ */
+$dbFile  = __DIR__ . '/../../../../data/usernames.sqlite3';
+$cfgFile = __DIR__ . '/../../../../config.php';
+if (file_exists($cfgFile)) {
+    include $cfgFile;
+}
+
+
+$ip = $_SERVER['REMOTE_ADDR'];
+if ($ip == '') {
+    //empty ip
+    header('X-Fail-Reason: empty ip address');
+    header('Content-type: application/json');
+    echo file_get_contents('me.json');
+    exit(1);
+}
+
+try {
+    $db = new SQLite3($dbFile, SQLITE3_OPEN_READONLY);
+} catch (Exception $e) {
+    //db file not found
+    header('X-Fail-Reason: database file not found');
+    header('Content-type: application/json');
+    echo file_get_contents('me.json');
+    exit(1);
+}
+
+$stmt = $db->prepare('SELECT * FROM usernames WHERE ip = :ip');
+$stmt->bindValue(':ip', $ip);
+$res = $stmt->execute();
+$row = $res->fetchArray(SQLITE3_ASSOC);
+$res->finalize();
+
+if ($row === false) {
+    header('Content-type: application/json');
+    echo file_get_contents('me.json');
+    exit();
+}
+
+$data = json_decode(file_get_contents('me.json'));
+$data->gamer->username = $row['username'];
+$data->gamer->nickname = $row['username'];
+
+switch (strtolower($row['username'])) {
+case 'cweiske':
+    $data->gamer->founder = true;
+    $data->gamer->avatar = $GLOBALS['baseUrl'] . 'avatars/cweiske.png';
+    break;
+case 'szeraax':
+    $data->gamer->founder = true;
+    break;
+}
+
+header('Content-type: application/json');
+echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
+?>
diff --git a/www/api/v1/gamers/me/index.html b/www/api/v1/gamers/me/index.html
deleted file mode 100644 (file)
index 1b1527b..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "gamer": {
-    "uuid": "00702342-0000-1111-2222-c3e1500cafe2",
-    "settings": {},
-    "founder": true,
-    "email": "stouyapi@example.org",
-    "username": "stouyapi"
-  }
-}
diff --git a/www/api/v1/gamers/register-error.json b/www/api/v1/gamers/register-error.json
new file mode 100644 (file)
index 0000000..e1de7e2
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "error": {
+        "message": "Validation errors occurred",
+        "code": 2006,
+        "data": {
+            "username": [
+                "Registration not available. Login instead."
+            ],
+            "email": [
+                "Registration not available - login instead. First part of login e-mail will be used as nickname."
+            ]
+        }
+    }
+}
diff --git a/www/api/v1/partner_builds.json b/www/api/v1/partner_builds.json
new file mode 100644 (file)
index 0000000..45671e9
--- /dev/null
@@ -0,0 +1,38 @@
+{
+    "actions": [
+        {
+            "action": "installFile",
+            "packageName": "tv.ouya.oobe",
+            "friendlyName": "OUYA OOBE",
+            "md5": "41d50891225591fdd32045ed0821a3bf",
+            "filesize": 7725861,
+            "downloadUrl": "http://ouya.cweiske.de/apks/ouya-everywhere/tv.ouya.oobe-1.2.897.apk",
+            "versionCode": 10200897
+        },
+
+        {
+            "action": "installFile",
+            "packageName": "tv.ouya",
+            "friendlyName": "OUYA Framework",
+            "md5": "95d987e7b100c2c72bdac37a8eda2647",
+            "filesize": 3123639,
+            "downloadUrl": "http://ouya.cweiske.de/apks/ouya-everywhere/tv.ouya-1.2.897.apk",
+            "versionCode": 10200897
+        },
+
+        {
+            "action": "installFile",
+            "packageName": "tv.ouya.console",
+            "friendlyName": "OUYA launcher",
+            "md5": "97da25989c64a90c60070978077fff55",
+            "filesize": 18953944,
+            "downloadUrl": "http://ouya.cweiske.de/apks/ouya-everywhere/tv.ouya.console-1.2.897.apk",
+            "versionCode": 10200897
+        },
+
+        {
+            "action": "launch",
+            "packageName": "tv.ouya.console"
+        }
+    ]
+}
diff --git a/www/api/v1/partner_builds.razer-forge-tv.json b/www/api/v1/partner_builds.razer-forge-tv.json
new file mode 100644 (file)
index 0000000..9a0b36f
--- /dev/null
@@ -0,0 +1,28 @@
+{
+    "actions": [
+        {
+            "action": "installFile",
+            "packageName": "tv.ouya",
+            "friendlyName": "Cortex Framework",
+            "md5": "697248296f967e428efc5c205c75d301",
+            "filesize": 6702855,
+            "downloadUrl": "http://ouya.cweiske.de/apks/razer-forge/CortexFramework-1.2.2320-tv.ouya.apk",
+            "versionCode": 10202320
+        },
+
+        {
+            "action": "installFile",
+            "packageName": "tv.ouya.console",
+            "friendlyName": "Razer Cortex",
+            "md5": "90637a8a5cbc937b3a11782838a3112d",
+            "filesize": 24220488,
+            "downloadUrl": "http://ouya.cweiske.de/apks/razer-forge/Razer_Cortex-1.2.2320-tv.ouya.console.apk",
+            "versionCode": 10202320
+        },
+
+        {
+            "action": "launch",
+            "packageName": "tv.ouya.console"
+        }
+    ]
+}
diff --git a/www/api/v1/partner_builds/release_notes b/www/api/v1/partner_builds/release_notes
new file mode 100644 (file)
index 0000000..ca321f2
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    "releaseNotes": "This is the latest firmware for the Razer Forge TV."
+}
index 43f7440054200655010d45600d61c400295b44ec..a044053502ef49d6793275a273e1c231122d5f22 100644 (file)
@@ -20,8 +20,15 @@ if ($ip == '') {
 }
 $ip = mapIp($ip);
 
+if (!isset($_GET['game'])) {
+    header('HTTP/1.0 400 Bad Request');
+    header('Content-type: text/plain');
+    echo 'Game parameter missing' . "\n";
+    exit(1);
+}
+
 $game = $_GET['game'];
-$cleanGame = preg_replace('#[^a-zA-Z0-9.]#', '', $game);
+$cleanGame = preg_replace('#[^a-zA-Z0-9._]#', '', $game);
 if ($game != $cleanGame || $game == '') {
     header('HTTP/1.0 400 Bad Request');
     header('Content-type: text/plain');
index b76532f28fcb55d4408eaee3a517bea3e154b4c3..3454a4d1b4d19a6a2c1506314a7fa5872e4bda95 100644 (file)
@@ -1,3 +1,3 @@
 {
-  "token": "00702342-0000-1111-2222-c3e1500cafe1"
+    "token": "00702342-0000-1111-2222-c3e1500cafe1"
 }
diff --git a/www/api/v1/sessions.php b/www/api/v1/sessions.php
new file mode 100644 (file)
index 0000000..39567ae
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Store the desired username during the login process
+ *
+ * It will be read by the ouya when calling api/v1/gamers/me.
+ *
+ * @author Christian Weiske <cweiske@cweiske.de>
+ * @see    api/v1/sessions
+ * @see    api/v1/gamers/me
+ */
+$dbFile  = __DIR__ . '/../../../data/usernames.sqlite3';
+
+if (!isset($_POST['username'])) {
+    header('HTTP/1.0 400 Bad Request');
+    header('Content-type: application/json');
+    echo '{"error":{"message":"Username missing","code": 2001}}' . "\n";
+    exit(1);
+}
+$username = $_POST['username'];
+
+$ip = $_SERVER['REMOTE_ADDR'];
+if ($ip == '') {
+    header('HTTP/1.0 400 Bad Request');
+    header('Content-type: application/json');
+    echo '{"error":{"message":"Cannot detect your IP address","code": 2002}}'
+        . "\n";
+    exit(1);
+}
+
+try {
+    $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE);
+} catch (Exception $e) {
+    header('HTTP/1.0 500 Internal server error');
+    header('Content-type: application/json');
+    echo '{"error":{"message":"Cannot open username database","code": 2003}}'
+        . "\n";
+    echo $e->getMessage() . "\n";
+    exit(2);
+}
+
+$res = $db->querySingle(
+    'SELECT name FROM sqlite_master WHERE type = "table" AND name = "usernames"'
+);
+if ($res === null) {
+    //table does not exist yet
+    $db->exec(
+        <<<SQL
+        CREATE TABLE usernames (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            username TEXT NOT NULL,
+            ip TEXT NOT NULL,
+            created_at TEXT DEFAULT CURRENT_TIMESTAMP
+        )
+SQL
+    );
+}
+
+//clean up old usernames
+$db->exec(
+    'DELETE FROM usernames'
+    . ' WHERE created_at < \'' . gmdate('Y-m-d H:i:s', time() - 86400) . '\''
+);
+
+//clean up previous logins
+$stmt = $db->prepare('DELETE FROM usernames WHERE ip = :ip');
+$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
+$stmt->execute()->finalize();
+
+//store the username
+$stmt = $db->prepare('INSERT INTO usernames (ip, username) VALUES(:ip, :username)');
+$stmt->bindValue(':ip', $ip);
+$stmt->bindValue(':username', $username);
+$res = $stmt->execute();
+if ($res === false) {
+    header('HTTP/1.0 500 Internal server error');
+    header('Content-type: application/json');
+    echo '{"error":{"message":"Cannot store username","code": 2004}}'
+        . "\n";
+    exit(3);
+}
+$res->finalize();
+
+header('HTTP/1.0 200 OK');
+header('Content-type: application/json');
+require __DIR__ . '/sessions';
+?>
index baf3104950112cf89bc6b6b44c1f8db9ddfed6ee..64a741ff75a5dfcc332c336e39fe0c04b87a4d66 100644 (file)
@@ -67,6 +67,7 @@ a {
 }
 .tile img {
     width: 20vw;
+    height: auto;
 }
 h3 {
     margin: 0;
diff --git a/www/updates-ouya_1_1.json b/www/updates-ouya_1_1.json
new file mode 100644 (file)
index 0000000..3b90a55
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "result": [
+    {
+      "filename": "OUYA-1.2.1084-r1.zip",
+      "timestamp": "1400714704",
+      "md5sum": "ed9f1712988c5ec2033f44fabe8e6297",
+      "channel": "stable",
+      "url": "http://devs-ouya-tv-prod.s3.amazonaws.com/ota/RC-OUYA-1.2.1084-r1_ota.zip",
+      "requiredUpto": "1.2.995",
+      "changes": "https://devs.ouya.tv/api/changelog/ed9f1712988c5ec2033f44fabe8e6297.txt",
+      "changesLocalized": {
+        "en": "<!doctype html>\r\n<html>\r\n    <head>\r\n        <meta charset=\"utf-8\">\r\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\r\n                <style>\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6,\r\n                    p,\r\n                    blockquote {\r\n                        margin: 0;\r\n                        padding: 0;\r\n                    }\r\n                    body {\r\n                        font-family: \"Helvetica Neue\", Helvetica, \"Hiragino Sans GB\", Arial, sans-serif;\r\n                        font-size: 13px;\r\n                        line-height: 18px;\r\n                        color: #FEFCFF;\r\n                        background-color: black;\r\n                        margin: 10px 13px 10px 13px;\r\n                    }\r\n                    table {\r\n                        margin: 10px 0 15px 0;\r\n                        border-collapse: collapse;\r\n                    }\r\n                    td,th {\r\n                        border: 1px solid #ddd;\r\n                        padding: 3px 10px;\r\n                    }\r\n                    th {\r\n                        padding: 5px 10px;\r\n                    }\r\n                    \r\n                    a {\r\n                        color: #0069d6;\r\n                    }\r\n                    a:hover {\r\n                        color: #0050a3;\r\n                        text-decoration: none;\r\n                    }\r\n                    a img {\r\n                        border: none;\r\n                    }\r\n                    p {\r\n                        margin-bottom: 9px;\r\n                    }\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6 {\r\n                        color: #404040;\r\n                        line-height: 36px;\r\n                    }\r\n                    h1 {\r\n                        margin-bottom: 18px;\r\n                        font-size: 30px;\r\n                    }\r\n                    h2 {\r\n                        font-size: 24px;\r\n                    }\r\n                    h3 {\r\n                        font-size: 18px;\r\n                    }\r\n                    h4 {\r\n                        font-size: 16px;\r\n                    }\r\n                    h5 {\r\n                        font-size: 14px;\r\n                    }\r\n                    h6 {\r\n                        font-size: 13px;\r\n                    }\r\n                    hr {\r\n                        margin: 0 0 19px;\r\n                        border: 0;\r\n                        border-bottom: 1px solid #ccc;\r\n                    }\r\n                    blockquote {\r\n                        padding: 13px 13px 21px 15px;\r\n                        margin-bottom: 18px;\r\n                        font-family:georgia,serif;\r\n                        font-style: italic;\r\n                    }\r\n                    blockquote:before {\r\n                        content:\"\\201C\";\r\n                        font-size:40px;\r\n                        margin-left:-10px;\r\n                        font-family:georgia,serif;\r\n                        color:#eee;\r\n                    }\r\n                    blockquote p {\r\n                        font-size: 14px;\r\n                        font-weight: 300;\r\n                        line-height: 18px;\r\n                        margin-bottom: 0;\r\n                        font-style: italic;\r\n                    }\r\n                    code, pre {\r\n                        font-family: Monaco, Andale Mono, Courier New, monospace;\r\n                    }\r\n                    code {\r\n                        background-color: #fee9cc;\r\n                        color: rgba(0, 0, 0, 0.75);\r\n                        padding: 1px 3px;\r\n                        font-size: 12px;\r\n                        -webkit-border-radius: 3px;\r\n                        -moz-border-radius: 3px;\r\n                        border-radius: 3px;\r\n                    }\r\n                    pre {\r\n                        display: block;\r\n                        padding: 14px;\r\n                        margin: 0 0 18px;\r\n                        line-height: 16px;\r\n                        font-size: 11px;\r\n                        border: 1px solid #d9d9d9;\r\n                        white-space: pre-wrap;\r\n                        word-wrap: break-word;\r\n                    }\r\n                    pre code {\r\n                        background-color: #fff;\r\n                        color:#737373;\r\n                        font-size: 11px;\r\n                        padding: 0;\r\n                    }\r\n                    sup {\r\n                        font-size: 0.83em;\r\n                        vertical-align: super;\r\n                        line-height: 0;\r\n                    }\r\n                    * {\r\n                        -webkit-print-color-adjust: exact;\r\n                    }\r\n                    @media screen and (min-width: 914px) {\r\n                        body {\r\n                            width: 854px;\r\n                            margin:10px auto;\r\n                        }\r\n                    }\r\n                    @media print {\r\n                        body,code,pre code,h1,h2,h3,h4,h5,h6 {\r\n                            color: black;\r\n                        }\r\n                        table, pre {\r\n                            page-break-inside: avoid;\r\n                        }\r\n                    }\r\n                    </style>\r\n                <title>OUYA System Update \"Chupacabra\"</title>\r\n                \r\n                </head>\r\n    <body>\r\n        \r\n        <font color=\"FF2A00\"><strong>\"Chupacabra\" Hotfix #2</strong></font>\r\n        <ul>\r\n            <li>Improvements to menu sounds to reduce choppiness.</li>\r\n            <li>Better pre-loading of tile art in DISCOVER.</li>\r\n            <li>Improved controller support in ODK (for OUYA Everywhere and to make it easier to bring apps and games to OUYA).</li>\r\n            <li>Fixed an issue where marking a game as favorite after searching for it could cause the tile art to not load.</li>\r\n        </ul>\r\n        \r\n        \r\n        <font color=\"FF2A00\"><strong>\"Chupacabra\" Hotfix #1</strong></font>\r\n        <ul>\r\n            <li>Sideloaded games in PLAY now say “(U) Launch” instead of “(U) Free Download”.</li>\r\n            <li>You can now navigate screenshots by pressing left and right while fullscreen.</li>\r\n            <li>Fixed an issue where external drives were showing less available space than they actually had.</li>\r\n            <li>Fixed an issue where recommended games weren’t showing in the launcher.</li>\r\n            <li>Fixed an issue where the BUY button wasn’t going away for games after redeeming a voucher.</li>\r\n            <li>Fixed an issue where downloading to external storage after a search could display a game title as “null”.</li>\r\n            <li>Fixed an issue where Developer pages could show a blank twitter handle.</li>\r\n            <li>Other various small fixes and optimizations.</li>\r\n        </ul>\r\n        \r\n        \r\n        \r\n        <font color=\"FF2A00\"><strong>OUYA System Update \"Chupacabra\" 4/14</strong></font>\r\n        \r\n        <ul>\r\n        <li>Audio passthrough is now supported in XBMC! Once you have the newest OUYA update, make sure you download the latest copy of XBMC on DISCOVER and you'll have audio passthrough support for AC3, DTS, and AAC.</li>\r\n        <li>Redesigned the Details page to provide a cleaner new look and feel.</li>\r\n        <ul>\r\n            <li>Now text is easier to read and screenshots are easier to navigate.</li>\r\n            <li>Reduced screen clutter to make navigation more clear.</li>\r\n            <li>No more stuff scrolling off the screen and looking like an overscan issue.</li>\r\n            <li>Page is now more versatile to support various types of pages like the Developer page discussed below.</li>\r\n        </ul>\r\n        <li>Reorganized PLAY to make it easier to find games.</li>\r\n        <ul>\r\n            <li>The top row will function as PLAY previously did.</li>\r\n            <li>We’ve removed the Thumbs Up system (too much overlap with ratings) and added a new “Favorite” system. Games you Favorite from the Details page will show up in the second row of PLAY.</li>\r\n            <li>Added a category for Purchased games, so you can more easily find games you’ve purchased (installed or not).</li>\r\n            <li>Added a category for Recent Downloads which will show currently downloading games plus the 10 most recent finished downloads.</li>\r\n            <li>Added an A-Z category so you can find games alphabetically.</li>\r\n        </ul>\r\n        <li>Added Developer-specific Details pages (and support for other generic Details pages).</li>\r\n        <ul>\r\n            <li>Now you can learn more about the folks who make the great games you love. Get to know your favorite devs!</li>\r\n            <li>These pages also allow an easy way to access all the games from a specific developer.</li>\r\n            <li>As Developers fill out their profiles, you'll start to see these pages appear on the console.</li>\r\n        </ul>\r\n        <li>Genre sections are now updated to have multiple rows.</li>\r\n        <ul>\r\n            <li>Instead of the previous “bucket”, Genres now have rows like the main page of DISCOVER, allowing the display of Featured games, New Releases, etc.</li>\r\n        </ul>\r\n        <li>Added a simple “download manager”.</li>\r\n        <ul>\r\n            <li>Games that are currently downloading will appear in the Recent Downloads section of PLAY (in addition to the places they previously appeared).</li>\r\n            <li>By pressing U on a game in the download queue, you can move games to the front of the queue to set the order in which you want your games to download. It will pause any current downloads and resume them from where they left off when the prioritized games are finished.</li>\r\n        </ul>\r\n        <li>Game developers can now choose if they want their games to be Free to Try or have a price up front.</li>\r\n        <li>Made pairing of Bluetooth media remotes easier.</li>\r\n        <ul>\r\n            <li>Using the PS3 Media Remote as an example:</li>\r\n            <ul>\r\n                <li>Navigate to the Android Settings located in Manage > Controllers > Pairing > Bluetooth Settings (Alternately, you can reach this by navigating to Manage > System > Advanced > Bluetooth)</li>\r\n                <li>Activate “Search For Device” to begin looking for pairing requests</li>\r\n                <li>On the Media Remote, press and hold START+ENTER buttons until the PS3 button begins flashing</li>\r\n                <li>When the device appears in the Available Devices list, select the BD Remote Control from the Available Devices list</li>\r\n                <li>The BD Remote Control should now appear in the Paired Devices list as connected.</li>\r\n            </ul>\r\n        </ul>\r\n        <li>Added an option to keep controllers turned on (no timed auto-shutoff) in select Media programs (like XBMC). This can be found in MANAGE -> CONTROLLER -> SETTINGS. This new option will be enabled by default.</li>\r\n        <li>Improvements to the new user flow</li>\r\n        <ul>\r\n            <li>Gamers no longer need to confirm their password as a new user</li>\r\n            <li>Added a date of birth field and optional gender field for new user signup</li>\r\n        </ul>\r\n        <li>We’ve made it more clear that if you are a user that would like to use PayPal, you can always purchase vouchers from OUYA.tv - pay there with PayPal, enter the code under MANAGE -> ACCOUNT -> PAYMENTS, and you are good to go!</li>\r\n        </ul>\r\n        \r\n    </body>\r\n</html>",
+        "es": "<!doctype html>\r\n<html>\r\n    <head>\r\n        <meta charset=\"utf-8\">\r\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\r\n                <style>\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6,\r\n                    p,\r\n                    blockquote {\r\n                        margin: 0;\r\n                        padding: 0;\r\n                    }\r\n                    body {\r\n                        font-family: \"Helvetica Neue\", Helvetica, \"Hiragino Sans GB\", Arial, sans-serif;\r\n                        font-size: 13px;\r\n                        line-height: 18px;\r\n                        color: #FEFCFF;\r\n                        background-color: black;\r\n                        margin: 10px 13px 10px 13px;\r\n                    }\r\n                    table {\r\n                        margin: 10px 0 15px 0;\r\n                        border-collapse: collapse;\r\n                    }\r\n                    td,th {\r\n                        border: 1px solid #ddd;\r\n                        padding: 3px 10px;\r\n                    }\r\n                    th {\r\n                        padding: 5px 10px;\r\n                    }\r\n                    \r\n                    a {\r\n                        color: #0069d6;\r\n                    }\r\n                    a:hover {\r\n                        color: #0050a3;\r\n                        text-decoration: none;\r\n                    }\r\n                    a img {\r\n                        border: none;\r\n                    }\r\n                    p {\r\n                        margin-bottom: 9px;\r\n                    }\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6 {\r\n                        color: #404040;\r\n                        line-height: 36px;\r\n                    }\r\n                    h1 {\r\n                        margin-bottom: 18px;\r\n                        font-size: 30px;\r\n                    }\r\n                    h2 {\r\n                        font-size: 24px;\r\n                    }\r\n                    h3 {\r\n                        font-size: 18px;\r\n                    }\r\n                    h4 {\r\n                        font-size: 16px;\r\n                    }\r\n                    h5 {\r\n                        font-size: 14px;\r\n                    }\r\n                    h6 {\r\n                        font-size: 13px;\r\n                    }\r\n                    hr {\r\n                        margin: 0 0 19px;\r\n                        border: 0;\r\n                        border-bottom: 1px solid #ccc;\r\n                    }\r\n                    blockquote {\r\n                        padding: 13px 13px 21px 15px;\r\n                        margin-bottom: 18px;\r\n                        font-family:georgia,serif;\r\n                        font-style: italic;\r\n                    }\r\n                    blockquote:before {\r\n                        content:\"\\201C\";\r\n                        font-size:40px;\r\n                        margin-left:-10px;\r\n                        font-family:georgia,serif;\r\n                        color:#eee;\r\n                    }\r\n                    blockquote p {\r\n                        font-size: 14px;\r\n                        font-weight: 300;\r\n                        line-height: 18px;\r\n                        margin-bottom: 0;\r\n                        font-style: italic;\r\n                    }\r\n                    code, pre {\r\n                        font-family: Monaco, Andale Mono, Courier New, monospace;\r\n                    }\r\n                    code {\r\n                        background-color: #fee9cc;\r\n                        color: rgba(0, 0, 0, 0.75);\r\n                        padding: 1px 3px;\r\n                        font-size: 12px;\r\n                        -webkit-border-radius: 3px;\r\n                        -moz-border-radius: 3px;\r\n                        border-radius: 3px;\r\n                    }\r\n                    pre {\r\n                        display: block;\r\n                        padding: 14px;\r\n                        margin: 0 0 18px;\r\n                        line-height: 16px;\r\n                        font-size: 11px;\r\n                        border: 1px solid #d9d9d9;\r\n                        white-space: pre-wrap;\r\n                        word-wrap: break-word;\r\n                    }\r\n                    pre code {\r\n                        background-color: #fff;\r\n                        color:#737373;\r\n                        font-size: 11px;\r\n                        padding: 0;\r\n                    }\r\n                    sup {\r\n                        font-size: 0.83em;\r\n                        vertical-align: super;\r\n                        line-height: 0;\r\n                    }\r\n                    * {\r\n                        -webkit-print-color-adjust: exact;\r\n                    }\r\n                    @media screen and (min-width: 914px) {\r\n                        body {\r\n                            width: 854px;\r\n                            margin:10px auto;\r\n                        }\r\n                    }\r\n                    @media print {\r\n                        body,code,pre code,h1,h2,h3,h4,h5,h6 {\r\n                            color: black;\r\n                        }\r\n                        table, pre {\r\n                            page-break-inside: avoid;\r\n                        }\r\n                    }\r\n                    </style>\r\n                <title>OUYA System Update \"Jackalope\"</title>\r\n                \r\n                </head>\r\n    <body>\r\n         \r\n        <font color=\"FF2A00\"><strong>Chupacabra: revisión #1</strong></font>\r\n        <ul>\r\n        <li>Ahora los juegos transferidos a Jugar ahora van acompañados del texto «(U) Iniciar» en lugar de «(U) Descarga gratuita».</li>\r\n        <li>Ahora podrás desplazarte por las capturas de pantallas pulsando izquierda y derecha en el modo a pantalla completa.</li>\r\n        <li>Se ha solucionado un error que hacía que los discos externos mostraran menos espacio disponible del que tenían en realidad.</li>\r\n        <li>Se ha solucionado un error que hacía que los juegos recomendados no aparecieran en la interfaz.</li>\r\n        <li>Se ha solucionado un error que hacía que el botón COMPRAR no desapareciera del juego incluso tras haber canjeado un cupón.</li>\r\n        <li>Se ha solucionado un error que hacía que al descargar un juego al disco externo tras una búsqueda, el título apareciera como «null» (nulo).</li>\r\n        <li>Se ha solucionado un error que hacía que en las páginas de los desarrolladores el nombre de usuario en Twitter apareciera en blanco.</li>\r\n        <li>Solución de otros errores y algunas mejoras.</li>\r\n        </ul>\r\n        \r\n        \r\n        \r\n        <font color=\"FF2A00\"><strong>Actualización del sistema OUYA: Chupacabra 04/14</strong></font>\r\n        \r\n        <ul>\r\n        <li>¡Ahora XBMC permite transferencias de audio! Una vez tengas la última actualización de OUYA, descárgate en DESCUBRIR la última versión de XBMC para poder realizar transferencias de audio en AC-3, DTS y AAC. </li>\r\n        <li>Hemos cambiado el diseño de la página de Detalles para ofrecer un aspecto y funcionamiento más simple.</li>\r\n        <li>Hemos reorganizado JUGAR para facilitar el acceso a los juegos.</li>\r\n        <ul>\r\n            <li>La hilera superior funcionará igual que solía hacerlo JUGAR. </li>\r\n            <li>Hemos eliminado el sistema «me gusta» (coincidía demasiado con las valoraciones) y añadido un nuevo sistema de «favoritos». Los juegos que marques como favoritos en la página de Detalles se mostrará en la segunda hilera de JUGAR.</li>\r\n            <li>Hemos añadido la categoría Juegos comprados para que encuentres más fácilmente los juegos que hayas adquirido (los tengas instalados o no).</li>\r\n            <li>Hemos añadido la categoría Últimas descargas, que mostrará tanto las descargas en curso como las últimas diez descargas completadas.</li>\r\n            <li>Hemos añadido la categoría A-Z para que encuentres los juegos alfabéticamernte.</li>\r\n        </ul>\r\n        <li>Hemos añadido páginas con información específica del desarrollador (y ayuda en otras páginas de información general).</li>\r\n        <ul>\r\n            <li>A medida que los desarrolladores completen sus perfiles, se mostrarán sus páginas en la consola. Estas páginas ofrecerán más información acerca de los desarrolladores y facilitarán la búsqueda de sus juegos.</li>\r\n        </ul>\r\n        <li>Las secciones de géneros de juegos están actualizadas y cuentan con múltiples hileras. </li>\r\n        <ul>\r\n            <li>En vez de la distribución anterior, ahora los géneros están ordenados por hileras, como la página principal de DESCUBRIR, que muestran los juegos destacados, novedades, etc.</li>\r\n        </ul>\r\n        <li>Hemos añadido un sencillo «administrador de descargas».</li>\r\n        <ul>\r\n            <li>Los juegos que te estés descargando aparecerán en una hilera específica en la sección Últimas descargas de JUGAR (además de los sitios en los que ya aparecían). </li>\r\n            <li>Si presionas U sobre un juego que esté en la cola de descargas, puedes desplazarlo al principio de la cola y cambiar el orden de descargas según tus preferencias. Las descargas que tengas en curso se detendrán y reanudarán cuando se hayan descargado los juegos a los que diste prioridad.</li>\r\n        </ul>\r\n        <li>Ahora, los desarrolladores de juegos pueden elegir si quieren que sus títulos se puedan probar de forma gratuita. </li>\r\n        <li>El emparejamiento por Bluetooth con mandos multimedia es ahora más sencillo.</li>\r\n        <ul>\r\n            <li>Pongamos el mando de PS3 como ejemplo:</li>\r\n            <ul>\r\n                <li>Navega por la configuración Android ubicada en Gestionar > Mandos > Emparejamiento > Configuración Bluetooth (también puedes encontrarla en Gestionar > Sistema > Opciones avanzadas > Bluetooth).</li>\r\n                <li>Activa «Buscar dispositivos» para iniciar la búsqueda de solicitudes de emparejamiento.</li>\r\n                <li>Pulsa y mantén START+ENTER en el mando multimedia hasta que los botones de la PS3 empiecen a parpadear.</li>\r\n                <li>Cuando el dispositivo aparezca en la lista de dispositivos disponibles, selecciona el mando a distancia de BD en la lista. </li>\r\n                <li>El mando a distancia de BD debería aparecer ahora como conectado en la lista de dispositivos emparejados.</li>\r\n            </ul>\r\n        </ul>\r\n        <li>Hemos añadido una opción para mantener los mandos encendidos (sin temporizador de apagado automático) en unas selección de programas multimedia, como el XBMC. Puedes encontrarla en Gestionar -> Mando -> Configuración. Esta nueva opción estará habilitada por defecto.</li>\r\n        <li>Mejoras en la inscripción de nuevos usuarios.</li>\r\n        <ul>\r\n            <li>Los jugadores ya no tienen que confirmar sus contraseñas como nuevos usuarios.</li>\r\n            <li>Hemos añadido el campo de fecha de nacimiento y otro opcional de sexo en la página de inscripción</li>\r\n        </ul>\r\n        <li>Puedes comprar cupones y pagarlos con PayPal en OUYA.tv. Paga tus cupones con tu cuenta PayPal en la página web e introduce el código en Gestionar -> Cuenta -> Pagos, ¡así de simple!</li>\r\n        </ul>\r\n        \r\n    </body>\r\n</html>",
+        "fr": "<!doctype html>\r\n<html>\r\n    <head>\r\n        <meta charset=\"utf-8\">\r\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\r\n                <style>\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6,\r\n                    p,\r\n                    blockquote {\r\n                        margin: 0;\r\n                        padding: 0;\r\n                    }\r\n                    body {\r\n                        font-family: \"Helvetica Neue\", Helvetica, \"Hiragino Sans GB\", Arial, sans-serif;\r\n                        font-size: 13px;\r\n                        line-height: 18px;\r\n                        color: #FEFCFF;\r\n                        background-color: black;\r\n                        margin: 10px 13px 10px 13px;\r\n                    }\r\n                    table {\r\n                        margin: 10px 0 15px 0;\r\n                        border-collapse: collapse;\r\n                    }\r\n                    td,th {\r\n                        border: 1px solid #ddd;\r\n                        padding: 3px 10px;\r\n                    }\r\n                    th {\r\n                        padding: 5px 10px;\r\n                    }\r\n                    \r\n                    a {\r\n                        color: #0069d6;\r\n                    }\r\n                    a:hover {\r\n                        color: #0050a3;\r\n                        text-decoration: none;\r\n                    }\r\n                    a img {\r\n                        border: none;\r\n                    }\r\n                    p {\r\n                        margin-bottom: 9px;\r\n                    }\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6 {\r\n                        color: #404040;\r\n                        line-height: 36px;\r\n                    }\r\n                    h1 {\r\n                        margin-bottom: 18px;\r\n                        font-size: 30px;\r\n                    }\r\n                    h2 {\r\n                        font-size: 24px;\r\n                    }\r\n                    h3 {\r\n                        font-size: 18px;\r\n                    }\r\n                    h4 {\r\n                        font-size: 16px;\r\n                    }\r\n                    h5 {\r\n                        font-size: 14px;\r\n                    }\r\n                    h6 {\r\n                        font-size: 13px;\r\n                    }\r\n                    hr {\r\n                        margin: 0 0 19px;\r\n                        border: 0;\r\n                        border-bottom: 1px solid #ccc;\r\n                    }\r\n                    blockquote {\r\n                        padding: 13px 13px 21px 15px;\r\n                        margin-bottom: 18px;\r\n                        font-family:georgia,serif;\r\n                        font-style: italic;\r\n                    }\r\n                    blockquote:before {\r\n                        content:\"\\201C\";\r\n                        font-size:40px;\r\n                        margin-left:-10px;\r\n                        font-family:georgia,serif;\r\n                        color:#eee;\r\n                    }\r\n                    blockquote p {\r\n                        font-size: 14px;\r\n                        font-weight: 300;\r\n                        line-height: 18px;\r\n                        margin-bottom: 0;\r\n                        font-style: italic;\r\n                    }\r\n                    code, pre {\r\n                        font-family: Monaco, Andale Mono, Courier New, monospace;\r\n                    }\r\n                    code {\r\n                        background-color: #fee9cc;\r\n                        color: rgba(0, 0, 0, 0.75);\r\n                        padding: 1px 3px;\r\n                        font-size: 12px;\r\n                        -webkit-border-radius: 3px;\r\n                        -moz-border-radius: 3px;\r\n                        border-radius: 3px;\r\n                    }\r\n                    pre {\r\n                        display: block;\r\n                        padding: 14px;\r\n                        margin: 0 0 18px;\r\n                        line-height: 16px;\r\n                        font-size: 11px;\r\n                        border: 1px solid #d9d9d9;\r\n                        white-space: pre-wrap;\r\n                        word-wrap: break-word;\r\n                    }\r\n                    pre code {\r\n                        background-color: #fff;\r\n                        color:#737373;\r\n                        font-size: 11px;\r\n                        padding: 0;\r\n                    }\r\n                    sup {\r\n                        font-size: 0.83em;\r\n                        vertical-align: super;\r\n                        line-height: 0;\r\n                    }\r\n                    * {\r\n                        -webkit-print-color-adjust: exact;\r\n                    }\r\n                    @media screen and (min-width: 914px) {\r\n                        body {\r\n                            width: 854px;\r\n                            margin:10px auto;\r\n                        }\r\n                    }\r\n                    @media print {\r\n                        body,code,pre code,h1,h2,h3,h4,h5,h6 {\r\n                            color: black;\r\n                        }\r\n                        table, pre {\r\n                            page-break-inside: avoid;\r\n                        }\r\n                    }\r\n                    </style>\r\n                <title>OUYA System Update \"Jackalope\"</title>\r\n                \r\n                </head>\r\n    <body>\r\n        \r\n        <font color=\"FF2A00\"><strong>Correctif \"Chupacabra\"</strong></font>\r\n        <ul>\r\n        <li>Les jeux en side-loading dans le menu JOUER indiquent dorénavant “(U) Lancer” au lieu de “(U) Téléchargement gratuit”</li>\r\n        <li>Vous pouvez dorénavant naviguer dans les captures d'écran en appuyant sur gauche et droite lorsque vous êtes en plein écran</li>\r\n        <li>Correction d'un problème qui affichait un montant d'espace disque libre incorrect sur les lecteurs externes</li>\r\n        <li>Correction d'un problème où les jeux recommandés n'étaient pas affichés dans le lanceur</li>\r\n        <li>Correction d'un problème où le bouton ACHETER ne disparaissait pas après l'échange d'un code-coupon</li>\r\n        <li>Correction d'un problème où le téléchargement vers les périphériques de stockage externe après une recherche pouvait ne pas afficher le titre du jeu</li>\r\n        <li>Correction d'un problème où les pages de Développeurs pouvaient montrer un nom de compte Twitter vide</li>\r\n        <li>Autres corrections mineures et optimisations</li>\r\n        </ul>\r\n        \r\n        \r\n        \r\n        <font color=\"FF2A00\"><strong>Mise à jour système OUYA \"Chupacabra\"</strong></font>\r\n        \r\n        <ul>\r\n        <li>L'intercommunication audio (passthrough) est maintenant pris en charge par XBMC ! Une fois la dernière mise à jour OUYA installée, assurez-vous de télécharger la dernière version de XBMC sur DÉCOUVRIR pour obtenir l'intercommunication audio vers AC3, DTS et AAC.</li>\r\n        <li>Réaménagement de la page Détails pour offrir une apparence plus propre et efficace.</li>\r\n        <li>Réorganisation de JOUER pour trouver des jeux plus facilement.</li>\r\n        <ul>\r\n            <li>La première rangée fonctionnera désormais de manière similaire à l'ancien JOUER.</li>\r\n            <li>Nous avons retiré le système d'approbation (redondant avec celui d'évaluation) et l'avons remplacé par un système de Favoris. Les jeux en favoris depuis la page Détails s'afficheront dans la seconde rangée de JOUER.</li>\r\n            <li>Ajout d'une catégorie pour les jeux achetés, afin de retrouver vos achats plus rapidement (installés ou non).</li>\r\n            <li>Ajout d'une catégorie pour les Téléchargements récents afin d'afficher les téléchargements en cours et les 10 jeux que vous avez téléchargé le plus récemment.</li>\r\n            <li>Ajout d'une catégorie A- Z pour retrouver vos jeux alphabétiquement.</li>\r\n        </ul>\r\n        <li>Ajout de pages Détails spécifiques aux développeurs (et une assistance sur les pages Détails génériques).</li>\r\n        <ul>\r\n            <li>À mesure que les développeurs rempliront leurs profils, vous verrez apparaître ces pages sur votre console. Celles-ci vous donneront plus d'informations sur les développeurs et une méthode simplifiée pour trouver leurs jeux.</li>\r\n        </ul>\r\n        <li>Section Genre mise à jour avec de nombreuses rangées. </li>\r\n        <ul>\r\n            <li>À la place d'une liste simple, les Genres sont maintenant organisés en rangées comme la page principale de DÉCOUVERTE, affichant les jeux par catégories : En vedette, Nouveautés, etc.</li>\r\n        </ul>\r\n        <li>Ajout d'un \"Gestionnaire de téléchargements\" simplifié.</li>\r\n        <ul>\r\n            <li>En pressant U sur un jeu en file de téléchargement, vous pouvez le placer au sommet de la file et ainsi définir l'ordre de téléchargement souhaité pour vos jeux. Ceci aura pour effet de mettre en pause tout téléchargement en cours, ne reprenant que lorsque le téléchargement prioritaire sera terminé.</li>\r\n        </ul>\r\n        <li>Les développeurs de jeux peuvent maintenant librement définir si leurs jeux sont Gratuits à essayer.</li>\r\n        <li>Appairage simplifié de télécommandes multimédia par Bluetooth. </li>\r\n        <ul>\r\n            <li>Exemple au moyen de la télécommande PS3 :</li>\r\n            <ul>\r\n                <li>Naviguez dans les paramètres Android situés dans Gérer > Manettes > Appairage > Configuration Bluetooth (ou alternativement, Gérer > Système > Avancé > Bluetooth).</li>\r\n                <li>Activez la \"Recherche de dispositifs\" pour trouver les requêtes d'appairage en attente.</li>\r\n                <li>Sur la télécommande multimédia, appuyez et maintenez les boutons DÉMARRER+ENTRER jusqu'à ce que le bouton PS3 se mette à clignoter.</li>\r\n                <li>Lorsque elle apparaît dans la liste des Dispositif disponibles, sélectionnez la Télécommande BD.</li>\r\n                <li>La télécommande BD devrait maintenant apparaître dans la liste en tant que dispositif connecté.</li>\r\n            </ul>\r\n        </ul>\r\n        <li>Ajout d'une option pour garder les manettes allumées (pas de désactivation automatique) dans certains programmes médias (comme XBMC). Cette option peut être trouvée dans GÉRER -> MANETTE -> CONFIGURATION. Notez que cette nouvelle option est activée par défaut.</li>\r\n        <li>Améliorations apportées à l'inscription d'un nouvel utilisateur.</li>\r\n        <ul>\r\n            <li>Les nouveaux utilisateurs n'ont désormais plus besoin de confirmer leur mot de passe.</li>\r\n            <li>Ajout d'une date de naissance et une catégorie optionnelle de genre lors de l'inscription d'un nouvel utilisateur.</li>\r\n        </ul>\r\n        <li>Si vous êtes un utilisateur désirant utiliser PayPal, nous avons augmenté la visibilité de pouvoir acheter en tout temps des coupons depuis OUYA.tv - payez-les avec PayPal, entrez le code dans GÉRER -> COMPTE -> PAIEMENTS, et vous voilà prêt !</li>\r\n        </ul>\r\n        \r\n    </body>\r\n</html>",
+        "it": "<!doctype html>\r\n<html>\r\n    <head>\r\n        <meta charset=\"utf-8\">\r\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\r\n                <style>\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6,\r\n                    p,\r\n                    blockquote {\r\n                        margin: 0;\r\n                        padding: 0;\r\n                    }\r\n                    body {\r\n                        font-family: \"Helvetica Neue\", Helvetica, \"Hiragino Sans GB\", Arial, sans-serif;\r\n                        font-size: 13px;\r\n                        line-height: 18px;\r\n                        color: #FEFCFF;\r\n                        background-color: black;\r\n                        margin: 10px 13px 10px 13px;\r\n                    }\r\n                    table {\r\n                        margin: 10px 0 15px 0;\r\n                        border-collapse: collapse;\r\n                    }\r\n                    td,th {\r\n                        border: 1px solid #ddd;\r\n                        padding: 3px 10px;\r\n                    }\r\n                    th {\r\n                        padding: 5px 10px;\r\n                    }\r\n                    \r\n                    a {\r\n                        color: #0069d6;\r\n                    }\r\n                    a:hover {\r\n                        color: #0050a3;\r\n                        text-decoration: none;\r\n                    }\r\n                    a img {\r\n                        border: none;\r\n                    }\r\n                    p {\r\n                        margin-bottom: 9px;\r\n                    }\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6 {\r\n                        color: #404040;\r\n                        line-height: 36px;\r\n                    }\r\n                    h1 {\r\n                        margin-bottom: 18px;\r\n                        font-size: 30px;\r\n                    }\r\n                    h2 {\r\n                        font-size: 24px;\r\n                    }\r\n                    h3 {\r\n                        font-size: 18px;\r\n                    }\r\n                    h4 {\r\n                        font-size: 16px;\r\n                    }\r\n                    h5 {\r\n                        font-size: 14px;\r\n                    }\r\n                    h6 {\r\n                        font-size: 13px;\r\n                    }\r\n                    hr {\r\n                        margin: 0 0 19px;\r\n                        border: 0;\r\n                        border-bottom: 1px solid #ccc;\r\n                    }\r\n                    blockquote {\r\n                        padding: 13px 13px 21px 15px;\r\n                        margin-bottom: 18px;\r\n                        font-family:georgia,serif;\r\n                        font-style: italic;\r\n                    }\r\n                    blockquote:before {\r\n                        content:\"\\201C\";\r\n                        font-size:40px;\r\n                        margin-left:-10px;\r\n                        font-family:georgia,serif;\r\n                        color:#eee;\r\n                    }\r\n                    blockquote p {\r\n                        font-size: 14px;\r\n                        font-weight: 300;\r\n                        line-height: 18px;\r\n                        margin-bottom: 0;\r\n                        font-style: italic;\r\n                    }\r\n                    code, pre {\r\n                        font-family: Monaco, Andale Mono, Courier New, monospace;\r\n                    }\r\n                    code {\r\n                        background-color: #fee9cc;\r\n                        color: rgba(0, 0, 0, 0.75);\r\n                        padding: 1px 3px;\r\n                        font-size: 12px;\r\n                        -webkit-border-radius: 3px;\r\n                        -moz-border-radius: 3px;\r\n                        border-radius: 3px;\r\n                    }\r\n                    pre {\r\n                        display: block;\r\n                        padding: 14px;\r\n                        margin: 0 0 18px;\r\n                        line-height: 16px;\r\n                        font-size: 11px;\r\n                        border: 1px solid #d9d9d9;\r\n                        white-space: pre-wrap;\r\n                        word-wrap: break-word;\r\n                    }\r\n                    pre code {\r\n                        background-color: #fff;\r\n                        color:#737373;\r\n                        font-size: 11px;\r\n                        padding: 0;\r\n                    }\r\n                    sup {\r\n                        font-size: 0.83em;\r\n                        vertical-align: super;\r\n                        line-height: 0;\r\n                    }\r\n                    * {\r\n                        -webkit-print-color-adjust: exact;\r\n                    }\r\n                    @media screen and (min-width: 914px) {\r\n                        body {\r\n                            width: 854px;\r\n                            margin:10px auto;\r\n                        }\r\n                    }\r\n                    @media print {\r\n                        body,code,pre code,h1,h2,h3,h4,h5,h6 {\r\n                            color: black;\r\n                        }\r\n                        table, pre {\r\n                            page-break-inside: avoid;\r\n                        }\r\n                    }\r\n                    </style>\r\n                <title>OUYA System Update \"Jackalope\"</title>\r\n                \r\n                </head>\r\n    <body>\r\n        \r\n        <font color=\"FF2A00\"><strong>\"Chupacabra\" Hotfix</strong></font>\r\n        <ul>\r\n        <li>I giochi side-load in GIOCA ora dicono “(U) Lancio” invece di“(U) Download gratuito”</li>\r\n        <li>Ora puoi sfogliare gli screenshot premendo a destra e sinistra quando sei in modalità schermo intero</li>\r\n        <li>Risolto un problema che mostrava meno spazio disponibile nelle memorie esterne di quanto ce ne fosse.</li>\r\n        <li>Risolto un problema che impediva di mostrare i giochi raccomandati sulla pagina di lancio</li>\r\n        <li>Risolto un problema che continuava a mostrare il tasto COMPRA su giochi anche dopo il riscatto di un voucher</li>\r\n        <li>Risolto un problema che faceva figurare vuoto un gioco dopo averlo scaricato su una memoria esterna</li>\r\n        <li>Risolto un problema che mostrava una barra twitter vuota nelle pagine degli Sviluppatori</li>\r\n        <li>Altre piccole migliorie di varia natura</li>\r\n        </ul>\r\n        \r\n        \r\n        <font color=\"FF2A00\"><strong>Aggiornamento sistema OUYA \"Chupacabra\"</strong></font>\r\n        \r\n        <ul>\r\n        <li>L'attraversamento audio ora è supportato in XBMC! Una volta che avrai l'ultimo aggiornamento OUYA, assicurati di scaricare l'ultima copia di XBMC su SCOPRI e avrai un passaggio audio che supporta AC3, DTS, e AAC.</li>\r\n        <li>Pagina dei dettagli ridisegnata per offrire un aspetto e un uso più diretto.</li>\r\n        <li>GIOCA riorganizzato per rendere più semplice trovare i giochi.</li>\r\n        <ul>\r\n            <li>La riga superiore continuerà a funzionare come GIOCA funzionava prima.</li>\r\n            <li>Abbiamo rimosso il sistema Pollice in su (troppa confusione coi voti) e aggiunto il sistema \"Preferiti\". I giochi che segnerai come Preferiti sulla pagina dei dettagli saranno mostrati nella seconda riga di GIOCA.</li>\r\n            <li>Aggiunta una categoria per i giochi acquistati, così puoi trovare più facilmente i giochi che hai già acquistato (che tu li abbia installati o meno).</li>\r\n            <li>Aggiunta la categoria dei Download recenti, che mostra i giochi che stai scaricando e gli ultimi 10 download completati.</li>\r\n            <li>Aggiunta la categoria A-Z per permetterti di trovare i giochi in ordine alfabetico. </li>\r\n        </ul>\r\n        <li>Aggiunte pagine dei dettagli speciali per gli sviluppatori (e supporto per le altre pagine dei dettagli generiche).</li>\r\n        <ul>\r\n            <li>Via via che gli sviluppatori riempiono i loro profili, comincerai a vedere apparire questa pagine sulla console. Esse ti danno più informazioni sugli sviluppatori e un modo più semplice di trovare i loro giochi.</li>\r\n        </ul>\r\n        <li>Le sezioni dei generi ora sono aggiornate per avere più righe. </li>\r\n        <ul>\r\n            <li>Al posto del precedente mucchio, i generi ora hanno righe come la pagina principale di SCOPRI, che permettono di mostrare i giochi in evidenza, i nuovi arrivi etc. </li>\r\n        </ul>\r\n        <li>Aggiunto un semplice “Gestione download”.</li>\r\n        <ul>\r\n            <li>Premendo U su un gioco in fila per il download, lo puoi spostare all’inizio della fila e stabilire l’ordine nel quale vuoi che i giochi siano scaricati. Questo metterà in pausa qualsiasi download in corso, che riprenderà da dove era stato interrotto una volta che il gioco prioritario avrà finito il download.</li>\r\n        </ul>\r\n        <li>Ora gli sviluppatore possono scegliere se potrai provare gratuitamente i loro giochi.</li>\r\n        <li>Abbiamo reso più semplice l’abbinamento coi telecomandi Bluetooth media. </li>\r\n        <ul>\r\n            <li>Per esempio, col PS3 remote puoi:</li>\r\n            <ul>\r\n                <li>Navigare alle impostazioni Android in Gestisci > Controller > Abbinamento > Impostazioni Bluetooth (Alternativamente, puoi usare il percorso Gestisci > Sistema > Avanzate > Bluetooth)</li>\r\n                <li>Attiva “Ricerca dispositivo” per cominciare a cercare richieste di abbinamento</li>\r\n                <li>Sul telecomando media, premi e tieni premuti i pulsanti START+ENTER finché il pulsante PS3 non comincia a lampeggiare</li>\r\n                <li>Quando il dispositivo compare sulla lista dei dispositivi disponibili, seleziona il telecomando BD dalla lista dei dispositivi disponibili</li>\r\n                <li>Il telecomando BD dovrebbe apparire nella lista dei dispositivi abbinati come connesso.</li>\r\n            </ul>\r\n        </ul>\r\n        <li>Aggiunta un’opzione per tenere i controller attivi (nessuno spegnimento a tempo) in alcuni programmi media (tipo XBMC). Puoi trovarla in Gestisci > Controller > Impostazioni. Quest’opzione sarà attivata automaticamente.</li>\r\n        <li>Miglioramenti al flusso nuovo utente</li>\r\n        <ul>\r\n            <li>I giocatori non dovranno più confermare la loro password come nuovi utenti</li>\r\n            <li>Aggiunti lo spazio della data di nascita e quello facoltativo del sesso per l’iscrizione dei nuovi utenti.</li>\r\n        </ul>\r\n        <li>Abbiamo reso più chiaro che se sei un nuovo utente che vorrebbe usare Paypal, puoi sempre acquistare voucher su OUYA.tv - pagali con PayPal, inserisci il codice sotto Gestione -> Account -> Pagamenti, e sei pronto a partire!</li>\r\n        </ul>\r\n        \r\n    </body>\r\n</html>",
+        "de": "<!doctype html>\r\n<html>\r\n    <head>\r\n        <meta charset=\"utf-8\">\r\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\r\n                <style>\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6,\r\n                    p,\r\n                    blockquote {\r\n                        margin: 0;\r\n                        padding: 0;\r\n                    }\r\n                    body {\r\n                        font-family: \"Helvetica Neue\", Helvetica, \"Hiragino Sans GB\", Arial, sans-serif;\r\n                        font-size: 13px;\r\n                        line-height: 18px;\r\n                        color: #FEFCFF;\r\n                        background-color: black;\r\n                        margin: 10px 13px 10px 13px;\r\n                    }\r\n                    table {\r\n                        margin: 10px 0 15px 0;\r\n                        border-collapse: collapse;\r\n                    }\r\n                    td,th {\r\n                        border: 1px solid #ddd;\r\n                        padding: 3px 10px;\r\n                    }\r\n                    th {\r\n                        padding: 5px 10px;\r\n                    }\r\n                    \r\n                    a {\r\n                        color: #0069d6;\r\n                    }\r\n                    a:hover {\r\n                        color: #0050a3;\r\n                        text-decoration: none;\r\n                    }\r\n                    a img {\r\n                        border: none;\r\n                    }\r\n                    p {\r\n                        margin-bottom: 9px;\r\n                    }\r\n                    h1,\r\n                    h2,\r\n                    h3,\r\n                    h4,\r\n                    h5,\r\n                    h6 {\r\n                        color: #404040;\r\n                        line-height: 36px;\r\n                    }\r\n                    h1 {\r\n                        margin-bottom: 18px;\r\n                        font-size: 30px;\r\n                    }\r\n                    h2 {\r\n                        font-size: 24px;\r\n                    }\r\n                    h3 {\r\n                        font-size: 18px;\r\n                    }\r\n                    h4 {\r\n                        font-size: 16px;\r\n                    }\r\n                    h5 {\r\n                        font-size: 14px;\r\n                    }\r\n                    h6 {\r\n                        font-size: 13px;\r\n                    }\r\n                    hr {\r\n                        margin: 0 0 19px;\r\n                        border: 0;\r\n                        border-bottom: 1px solid #ccc;\r\n                    }\r\n                    blockquote {\r\n                        padding: 13px 13px 21px 15px;\r\n                        margin-bottom: 18px;\r\n                        font-family:georgia,serif;\r\n                        font-style: italic;\r\n                    }\r\n                    blockquote:before {\r\n                        content:\"\\201C\";\r\n                        font-size:40px;\r\n                        margin-left:-10px;\r\n                        font-family:georgia,serif;\r\n                        color:#eee;\r\n                    }\r\n                    blockquote p {\r\n                        font-size: 14px;\r\n                        font-weight: 300;\r\n                        line-height: 18px;\r\n                        margin-bottom: 0;\r\n                        font-style: italic;\r\n                    }\r\n                    code, pre {\r\n                        font-family: Monaco, Andale Mono, Courier New, monospace;\r\n                    }\r\n                    code {\r\n                        background-color: #fee9cc;\r\n                        color: rgba(0, 0, 0, 0.75);\r\n                        padding: 1px 3px;\r\n                        font-size: 12px;\r\n                        -webkit-border-radius: 3px;\r\n                        -moz-border-radius: 3px;\r\n                        border-radius: 3px;\r\n                    }\r\n                    pre {\r\n                        display: block;\r\n                        padding: 14px;\r\n                        margin: 0 0 18px;\r\n                        line-height: 16px;\r\n                        font-size: 11px;\r\n                        border: 1px solid #d9d9d9;\r\n                        white-space: pre-wrap;\r\n                        word-wrap: break-word;\r\n                    }\r\n                    pre code {\r\n                        background-color: #fff;\r\n                        color:#737373;\r\n                        font-size: 11px;\r\n                        padding: 0;\r\n                    }\r\n                    sup {\r\n                        font-size: 0.83em;\r\n                        vertical-align: super;\r\n                        line-height: 0;\r\n                    }\r\n                    * {\r\n                        -webkit-print-color-adjust: exact;\r\n                    }\r\n                    @media screen and (min-width: 914px) {\r\n                        body {\r\n                            width: 854px;\r\n                            margin:10px auto;\r\n                        }\r\n                    }\r\n                    @media print {\r\n                        body,code,pre code,h1,h2,h3,h4,h5,h6 {\r\n                            color: black;\r\n                        }\r\n                        table, pre {\r\n                            page-break-inside: avoid;\r\n                        }\r\n                    }\r\n                    </style>\r\n                <title>OUYA System Update \"Jackalope\"</title>\r\n                \r\n                </head>\r\n    <body>\r\n        \r\n        <font color=\"FF2A00\"><strong>\"Chupacabra\"-Hotfix</strong></font>\r\n        <ul>\r\n        <li>Die Option \"(U) Gratis-Download\" für nicht offizielle (side-loaded) Spiele im Menü \"Jetzt SPIELEN\" wurde durch \"(U) Starten\" ersetzt.</li>\r\n        <li>Durch Drücken der linken und rechten Richtungstaste kann im Vollbildmodus jetzt zwischen Screenshots umgeschaltet werden.</li>\r\n        <li>Problem behoben, bei dem externe Speichergeräte weniger Platz angezeigt haben, als verfügbar war.</li>\r\n        <li>Problem behoben, bei dem empfohlene Spiele nicht im Launcher angezeigt wurden.</li>\r\n        <li>Problem behoben, bei dem die KAUFEN-Schaltfläche für Spiele nach Einlösen eines Gutscheins nicht ausgeblendet wurde.</li>\r\n        <li>Problem behoben, bei dem der Titel eines Spieles nach dem Herunterladen auf ein externes Speichergerät, bei einer Suchanfrage als \"null\" angezeigt werden konnte.</li>\r\n        <li>Problem behoben, bei dem auf Entwicklerseiten der Twitter-Name leer blieb.</li>\r\n        <li>Weitere diverse kleine Fixes und Optimierungen.</li>\r\n        </ul>\r\n            \r\n        <font color=\"FF2A00\"><strong>OUYA-System-Update \"Chupacabra\"</strong></font>\r\n        \r\n        <ul>\r\n        <li>XBMC unterstützt jetzt Audiosignaldurchleitung! Sobald du das aktuellste OUYA-Update hast, solltest du unter ENTDECKEN die neueste Version von XBMC herunterladen, und schon wird Audiosignaldurchleitung für AC3, DTS und AAC unterstützt.</li>\r\n        <li>Die Details-Seite wurde überarbeitet und erstrahlt in neuem, klarerem Design.</li>\r\n        <li>Das Menü SPIELEN wurde neu gestaltet, sodass es jetzt leichter ist, Spiele zu finden.</li>\r\n        <ul>\r\n            <li>Die oberste Reihe übernimmt jetzt die Funktion, die SPIELEN früher hatte.</li>\r\n            <li>Wir haben das \"Thumbs Up\"-System abgeschafft (wegen zu großer Überschneidungen mit dem Bewertungssystem) und ein neues Favoriten-System eingeführt. Spiele, die du auf der Details-Seite als Favorit markierst, werden in der zweiten Reihe unter SPIELEN gelistet.</li>\r\n            <li>Kategorie für gekaufte Spiele hinzugefügt. So kannst du die von dir gekauften Spiele leichter finden, egal ob diese installiert sind oder nicht.</li>\r\n            <li>Kategorie für zuletzt heruntergeladene Spiele hinzugefügt. Hier werden Spiele, die derzeit heruntergeladen werden sowie die 10 zuletzt abgeschlossenen Downloads angezeigt.</li>\r\n            <li>A-Z-Kategorie hinzugefügt, damit du Spiele alphabetisch anzeigen lassen kannst.</li>\r\n        </ul>\r\n        <li>Entwickler-spezifische Details-Seiten hinzugefügt (und Unterstützung für andere, generische Details-Seiten).</li>\r\n        <ul>\r\n            <li>Wenn Entwickler ihre Profile ausfüllen, wirst du diese Seiten auf deiner Konsole betrachten können. Diese Seiten bieten mehr Infos über Entwickler und erleichtern es, deren Spiele zu finden.</li>\r\n        </ul>\r\n        <li>Genre-Bereiche verfügen jetzt über mehrere Reihen.</li>\r\n        <ul>\r\n            <li>Genres verfügen jetzt über Reihen wie die ENTDECKEN-Hauptseite, um Spiele-Highlights, Neuveröffentlichungen usw. anzeigen zu können.</li>\r\n        </ul>\r\n        <li>Einfacher \"Download-Manager\" hinzugefügt.</li>\r\n        <ul>\r\n            <li>Wenn du bei einem Spiel in der Download-Warteliste auf U drückst, kannst du es an die Spitze der Liste setzen, um die Reihenfolge zu beeinflussen, in der deine Spiele heruntergeladen werden sollen. Laufende Downloads werden unterbrochen und erst dann fortgesetzt, nachdem die bevorzugten Spiele vollständig heruntergeladen wurden.</li>\r\n        </ul>\r\n        <li>Spielentwickler können jetzt festlegen, dass ihre Spiele kostenlos ausprobiert werden können.</li>\r\n            <li>Die Synchronisierung von Bluetooth-Fernbedienungen ist jetzt einfacher.</li>\r\n        <ul>\r\n            <li>So funktioniert es zum Beispiel mit der PS3-Fernbedienung:</li>\r\n            <ul>\r\n                <li>Gehe zu den Android-Einstellungen in Verwalten > Controller > Synchronisierung > Bluetooth-Einstellungen (alternativ erreichst du dieses Menü auch über Verwalten > System > Erweitert > Bluetooth).</li>\r\n                <li>Aktiviere \"Gerät suchen\", um nach Synchronisierungsanfragen zu suchen.</li>\r\n                <li>Drücke und halte die START- und ENTER-Taste auf der Fernbedienung gedrückt, bis die PS3-Taste zu blinken beginnt.</li>\r\n                <li>Wenn das Gerät in der Liste der verfügbaren Geräte erscheint, wähle die BD-Fernbedienung aus.</li>\r\n                <li>Die BD-Fernbedienung sollte jetzt in der Liste der synchronisierten Geräte als verbunden angezeigt werden.</li>\r\n            </ul>\r\n        </ul>\r\n        <li>Option hinzugefügt, dass Controller in bestimmten Medienspielern (wie z. B. XBMC) eingeschaltet bleiben (kein automatisches Abschalten nach einer gewissen Zeit). Diese neue Option befindet sich in VERWALTEN -> CONTROLLER -> EINSTELLUNGEN und ist standardmäßig aktiviert.</li>\r\n        <li>Der Kontoerstellungsvorgang für neue Benutzer wurde vereinfacht.</li>\r\n        <ul>\r\n            <li>Neue Benutzer müssen ihr Passwort nicht erneut bestätigen.</li>\r\n            <li>Eingabefeld für Geburtsdatum und optionales Eingabefeld für Geschlecht zum Kontoerstellungsvorgang für neue Benutzer hinzugefügt.</li>\r\n        </ul>\r\n        <li>Für Benutzer, die gerne mit PayPal bezahlen wollen, ist jetzt deutlicher erkennbar, dass sie jederzeit Gutscheine auf OUYA.tv kaufen können. Dort kannst du mit PayPal bezahlen und den erhaltenen Code dann unter VERWALTEN -> KONTO -> ZAHLUNGEN eingeben, und fertig!</li>\r\n        </ul>\r\n        \r\n    </body>\r\n</html>"
+      },
+      "incremental": false
+    }
+  ]
+}