Outils pour utilisateurs

Outils du site


portrait-robot

Portrait-robot

Lien GitLab du projet

Partie concept

Intention

Mon objectif est d’utiliser mon Raspberry Pi comme d’une personne qui utiliserait divers réseaux sociaux en suivant uniquement ce que les algorithmes de recommandation lui conseillent. Je veux créer principalement un compte Google qui me permettrait de créer les autres comptes en les liant à celui-là. Mon programme tournerait en continu sur le Raspberry et regarderait en boucle des vidéos sur Youtube, écouterait une playlist sur-mesure sur Spotify, suivrait les comptes recommandés par Twitter, etc.
Je veux voir ce que les différents algorithmes vont créer comme personnalité à ce faux compte.
C’est une boucle de rétroaction crée par l’usage  : l’utilisation même détermine cette utilisation.

Scénarios

Pour l’instant le scénario de mon “utilisateur qui ne choisit pas” (appelons-le Philippe) est de se connecter sur un réseau social, cliquer sur le premier lien disponible et de suivre sans interruption ce que lui propose les algorithmes de recommandation pendant une durée indéfinie.
C’est un comportement protocolaire qui n’est pas très réaliste, mais il évoque bien la "zone de la machine".

Sur Youtube, il clique sur le premier lien de la première ligne et continue à regarder ce qui se trouve dans la case “up-next”. S’il s’arrête (quand je le débranche), la fois d’après il reprend à partir de la dernière vidéo dans le log (un fichier texte qui s’agrandit de liens au fur et à mesure).

Sur Spotify même principe, sauf que le bot enregistre le titre/le nom de l’artiste pour chaque morceau.

Sur Twitter, il clique sur la première personne recommandée dans “Who follow  ?” et prend une capture d’écran et enregistre le nom de la personne en question. Il s’abonne/aime le profil pour que l’algorithme de Twitter affine ses préférences. Philippe saute de personne en personne indéfiniment.

Sur Facebook même principe que Twitter.

L’objectif maintenant c’est de trouver une narration, de fictionnaliser le comportement de Philippe pour le rendre plus plausible en tant que personne, pour être plus raccord avec mon propos.

Quel autre comportement Philippe peut-il avoir  ?

  • Cliquer sur une vidéo/un article/un post au hasard parmi la première page  ? Parmi la première ligne  ?
  • Cliquer sur la vidéo la plus “visible”, attirante à l’oeil (avec un titre uniquement en CAPITALE  ? Le thumbnail avec le plus de contraste  ?)
  • Regarder des vidéos/articles/posts en boucle à l’infini  ?
  • Seulement pendant des horaires de bureau, comme si c’était un bullshit job  ? Est-ce que son “emploi du temps” est séparé avec 2h de Facebook/2h de YouTube/2h d’Instagram/2h de Twitter  ?
  • Regarder du contenu seulement pendant la nuit, pendant l’inverse des horaires de bureau → comme pour une passion, un seul divertissement en dehors du cadre du travail  ?
  • Le faire se comporter comme un alter ego qui est actif quand je ne suis pas actif  ? (Quand je dors globalement)
  • Est-ce que Philippe reste une longue période sur la même plateforme avant de changer  ? Quelle durée  ? 1 semaine  ? 1 mois  ?
  • Est-ce que Philippe se déplace (migrations pendulaires  ?)  ? Est-ce qu’il peut voyager entre les pays en prenant le bus VPN ou rentrer en France pendant les vacances  ?
  • Est-ce que Philippe change d’ordi de temps en temps et s’active sur le mien ou celui de mes colocs  ?
  • Est-ce que Philippe passe les pubs ou bien est-ce que’il les regarde toutes assidûment (voire même clique dessus)  ?

Réfléchir à la relation entre “l’agentivité” de l’usager, sa capacité à prendre des décisions et à agir, et la construction d’une figure abstraite complètement déterminée par l’algorithme  ?

Apparemment malgré ces dispositifs de facilitation/suppression du choix et de l’autonomie, les utilisateurs réels sont la plupart du temps quand même en autonomie. Ils n’ont recourt que ponctuellement aux algorithmes de recommandation, parfois dans des contextes bien particuliers (quand ils écoutent de la musique en arrière-plan en faisant autre chose par exemple.
(cf. article LES ALGORITHMES DE RECOMMANDATION MUSICALE ET L’AUTONOMIE DE L’AUDITEUR, Jean-Samuel Beuscart, Samuel Coavoux et Sisley Maillard)

L’article soulève aussi la question du goût  : est-ce que ces algorithmes créent des utilisateurs dépassionnés, sans jugement de goût et sans réelles préférences  ?

Questionner l’objectif dans l’utilisation de certains réseaux  : Twitter plus politique que Youtube ou Spotify  ? Quel partie de la personnalité je veux construire à travers ce projet, dans quel domaine je veux que Philippe ait une opinion  ?

Philippe ?

Est-ce que Philippe est le bon nom  ? Est-ce que genrer Philippe est une bonne idée, ou bien un prénom mixte (comme Dominique) correspondrait mieux vu que l’on parle d’un bot/programme  ? Liste de prénoms mixtes (qui s’écrivent pareil) d’après Wikipédia (j’ai enlevé les moins ambigus)  :

  • Dominique
  • Camille
  • Alex
  • Alix
  • Sacha
  • Ange
  • Claude
  • Lou
  • Candide
  • Charlie (apparemment le mieux réparti au niveau de la parité)
  • Yaël (bien réparti aussi)
  • Céleste
  • Andrea
  • Louison (lui aussi)
  • Mahé
  • Gwenn
  • Loan
  • Philippe (apparemment très rarement féminin)

En clair, le problème est comment ne pas rattacher le bot à une norme de représentation.

Cependant le programme va tourner sur un Raspberry en Belgique, il sera situé et sa navigation en sera impactée, donc je pourrais peut-être choisir un prénom mixte en fonction des statistiques de prénoms belges  ? L’idée est de partir d’une base d’utilisateur.ice lambda, dans la moyenne, pour voir ce que les algorithmes font de cette base.

Est-ce que le bot a même besoin d’un prénom  ? La raison pour laquelle j’y tiens c’est pour l’adresse mail et tout ce qui tourne autour, et que pour moi la personnalité/l’identité s’ancrent dans le prénom.
Pourquoi donner des noms aux choses est compliqué ?

En cherchant des statistiques ethnographiques sur les catégories de personnes qui regardent YouTube, je suis tombé sur cet article de Google, qui vante les mérites de YouTube. Il nous apprend seulement que plus de la moitié des gens entre 16 et 44 ans en France vont tous les jours sur YouTube (2016). Mais il nous renseigne sur la façon dont Google veut que YouTube soit perçu  : un lieu d’ouverture, de partage, de multiplicité des choix, de liberté, et donc pas du tout comme je veux le montrer  : une machine qui restreint les choix et peut enfermer dans une bulle de filtre.

Dans les prénoms mixtes, j’aime bien Dominique pour faire un jeu de mot avec le DOM (pas super pertinent mais renvoie au web), mais le prénom est un peu vieillot (et donc ne rentre pas trop dans la catégorie 18-35 ans qui sont les plus actifs sur YouTube).

J’aime aussi Gwenn, qui sonne bien comme diminutif de pleins de prénoms possibles. Aussi, si je considère le bot comme mon espèce d’alter ego (puisqu’il me suit physiquement et est localisé dans mon appartement), le fait que ce soit un prénom d’origine bretonne colle bien.
Définition d’ épicène.

Mais ces prénoms sont assez localisés géographiquement/culturellement, et j’ai un peu un dilemme entre un prénom choisi pour sa neutralité ou avec des statistiques (mais rien n’est vraiment neutre), ou alors un prénom qui donne déjà une connotation/un ancrage culturel.

Pour l’adresse mail et pour créer un compte (nom d’utilisateur et tout), il faudrait peut-être aussi un numéro, style gwenn58@gmail.com, et un nom de famille.
Quel numéro peut avoir du sens  ? Souvent c’est une date de naissance ou un numéro lié à l’endroit où l’on habite.

Décisions

Horaires

Je veux que le bot soit comme une personne qui travaille avec des horaires de bureaux toutes la semaine, et utilise les réseaux sociaux comme moyen principal de divertissement/comme hobby.
Ses horaires de bureau seraient 8h30-12h et 13h30-17h30, du lundi au vendredi. Le bot serait actif sur la pause de midi, le soir et le weekend, soit :

  • de 12h30-13h30, 18h30-19h30 et 21h-23h du lundi au vendredi
  • 10h-12h, 13h-19h30, 21h-00h le weekend

(en comptant des moments de repas + sommeil).

Il faut que je me renseigne sur cron pour la planification des horaires de fonctionnement du programme.

Matérialité

Le programme tournerait sur mon Raspberry Pi, le bot aurait donc une matérialité physique. Il se déplace avec moi si je change d’appartement,

Localisation

Le Raspberry fonctionnerait dans mon appartement, à Bruxelles. Il dépend de ma localisation.

Réseaux

Dans un premier temps le bot utiliserait uniquement YouTube (car je ne suis pas sûr d’avoir le temps de creuser le projet avec Spotify ou Twitter par exemple), mais mon envie de départ était que le bot soit multi-plateforme.

Pubs

Pour l’instant le bot utilise une extension qui passe les pubs (je ne suis pas sûr de la fiabilité de cet article mais cela semble assez répandu pour que je le considère sur une personne “lambda”.

Le doute ?

Je me prend un peu la tête sur la définition de la personnalité du bot/sa personnalité pour le rendre humain, mais ce qui m’intéresse c’est plus les résultats que l’expérience va produire.
Je vais plutôt me concentrer sur des protocoles multiples, pour voir les différents types de résultats ça peut produire. Plutôt que de me concentrer sur un choix ultime, je m’autorise à changer de compte par exemple, en changeant les paramètres du bot. Le numérique me permet une démultiplication des identités du bot, qu’il ait des alias, des forks, et que finalement c’est plus intéressant que d’imiter de manière automatique un comportement humain.
Le nom du bot peut être multiple, et je cherche un nom de figure/personnage fictif, dont l’histoire incarnerait les thèmes de mon projet.

J’ai pensé ironiquement à un nom de personnage qui fait face à un dilemme cornélien, alors que le bot fait le moins de choix possible.

Pour ce premier bot j’ai choisi Cinna, et puisque la pièce de Corneille a été écrite en 1643 son compte Google sera cinna1643@gmail.com.
Ce bot tourne 24/24h, regarde les vidéos en entier et passe les pubs.

Mise en forme

Vidéo

Avec les 22 premières vidéos que j’ai récupérées, je voulais faire une sorte de méga mashup en fusionnant 1 seconde de chaque vidéo, pour faire une sorte de parcours condensé du parcours du bot.
Pour cela j’ai utilisé ffmpeg, qui permet de faire des manipulations vidéo sur plein de fichiers en même temps (comme ImageMagick pour les images par exemple) en ligne de commande.
J’ai essayé de faire un script bash qui permet de couper chaque vidéo pour garder 1 seconde, et ensuite de fusionner toutes ces vidéos entre elles. Je pense que j’ai des problèmes de codecs puisque l’audio est décalé et qu’à un moment l’écran devient complètement vert. Toutes les vidéos YouTube ne sont pas dans le même format étonnamment, je pense que c’est une question de réglages. Avec ffmpeg on peut aussi faire des filtres pour convertir d’un coup plusieurs vidéos dans le même codec mais ça m’a l’air compliqué, je vais creuser.

J’ai réussi à faire une version test avec Premiere Pro, mais dans l’idée il faudrait que je puisse le faire automatiquement avec ffmpeg sur le RPi. Peut-être que le traitement (téléchargement/coupe/fusion des contenu) peut se faire pendant que Philippe est “off” (qu’il ne parcourt pas de contenu)  ?

Le résultat n’est pas très satisfaisant. Trop rapide ou trop direct.

Mise en espace/installation

Pour l’instant quand je lance Selenium il y a une interface graphique qui se lance, donc je peux voir sans toucher à rien le bot qui s’active et “fait sa vie”.
Cette étape est assez fascinante et flippante, c’est un peu comme si Philippe prenait le contrôle et lançait des vidéos comme ça sur mon ordi pendant que je travaille dessus. Alors sur Youtube une fois le premier clic sur une vidéo passé c’est pas très intéressant, mais sur un autre réseau avec plus d’interactivité (scroll par exemple  ?) cette vidéo peut-être parlante, et agir presque comme un test de Turing si on la voit sans le contexte.

Problème  : comment faire pour que ça n’ait pas l’air d’une simple vidéo en accéléré de quelqu’un sur un ordi  ? Une possibilité serait de le montrer en live, et donc de donner un côté performatif à l’expérience.

  • Peut-être que le programme peut être lancé sur un ordi (avec crontab  ?) qui l’active/l’arrête à des heures fixes (et donc on verrait les prints dans un terminal de cron et du programme, quand selenium n’est pas actif  ?
  • Il faudrait pouvoir visualiser d’une façon ou d’une autre le clic ou le mouvement de souris, pour retrouver ce côté automatique qu’il y a dans le remplissage du formulaire.
  • Peut-être qu’il peut-être streamé en direct depuis cet ordi  ? Philippe aurait une chaîne Twitch pour partager sa passion de la recommandation  ?

Voir le déroulement en live accentue le côté voyeur, comme si on espionnait quelqu’un devant son ordi pendant qu’il se détend, drôle de sensation. L’objectif est de nous mettre dans une position de voyeur alors qu’on a affaire a un robot.

Matériellement, quel dispositif pour montrer cette vidéo  ?

  • Il faudrait un environnement de bureau, un fauteuil avec du café/thé  ?), personnalisé (peut-être par rapport aux passions que Philippe a acquis pendant l’expérience). Ambiance casual et intime (le voyeurisme tout ça).
  • Quel type d’ordi  ? Philippe est un “monsieur tout le monde” Un article qui peut donner une piste ?
  • Dans une petite pièce, un vrai bureau ou dans une pièce qui n’a rien à voir  ?

Peut-être que toutes mes expérimentations d’AP peuvent se retrouver sur ce bureau (éditions, sites/interfaces dans d’autres fenêtres…), et alors on fouillerait dans ses tiroirs et sur son bureau pour enquêter sur lui  ? (lien avec le portrait-robot  ?)

La question mon alter-ego revient, puisque les intérêts de Philippe seraient alors confondus avec les miens (mon sujet de la recherche de cette année). Il pourrait alors y avoir deux bureaux, un bureau qui est celui de Philippe étant le résultat et fonctionnant indépendamment, et mon bureau, avec les dessous, les projets parallèles et connexes qui viennent nourrir le projet et apporter de l’interactivité (expériencer des sites sans recommandation, aller sur le “blog” de Philippe ou voir les logs du RPi).

Interface

L’idée serait peut-être aussi de faire une interface, comme un journal de bord de ce parcours à travers les algorithmes (en modifiant l’interface de la Pibliothèque  ?). Avec les logs récupérés (URLs…), je peux mettre en page dynamiquement comme un blog, un agrégat qui serait le reflet de la personnalité de Philippe.
Comment montrer cette personnalité, ce “portrait-robot”  ? Comment rendre des logs narratifs  ?

Je pourrais éventuellement intégrer une partie d’étude statistique (à partir des tags par exemple) pour essayer de trouver des tendances dans le comportement de Philippe, un peu comme une étude sociologique de son profil. Ça peut renvoyer à la surveillance et au côté analytique des algorithmes, qui construisent ce genre de profil statistique pour construire des recommandations personnalisées, et en même temps être un moyen différent de montrer une personnalité.

Peut-être qu’une façon de rendre compte du parcours est d’enregistrer sous forme de phrases les différentes actions effectuées par le bot (Je clique à tel endroit, j’attends que la vidéo se termine, la page se recharge). Lecture par un text-to-speech pour évoquer l’aide pour les non-voyants (le narrateur vocal  ?)  ? Voix qui décrit à l’oral le comportement de l’utilisateur  ?

J’ai essayé de donner une forme narrative à ce “log” (fichier texte qui regroupe différents messages concernant le fonctionnement d’un programme) :

Je me connecte tout seul, c'est bien pratique
Je regarde la vidéo de la dernière fois.
Je clique sur le bouton "play" pour lancer la vidéo.
  Le titre de la vidéo est Et voilà les Shadoks, la saison 2 | Archive INA.
    L'url est [https://www.youtube.com/watch?v=Dk1JjjbZ4yc]
      La vidéo dure 2:18:45.
 
Une nouvelle vidéo !
  Le titre de la vidéo est Et voilà les Shadoks, la saison 1 | Archive INA.
    L'url est [https://www.youtube.com/watch?v=tpD0Pdr7oD0]
      La vidéo dure 1:33:51.
 
Une nouvelle vidéo !
  Le titre de la vidéo est Culte : c’était leur 1ère télé, allez-vous les reconnaître ? | Archive INA.
    L'url est [https://www.youtube.com/watch?v=XA3ScOmGr34]
      La vidéo dure 25:29.
 
Une nouvelle vidéo !
  Le titre de la vidéo est 5 inventions cultes des années 90 | Archive INA.
    L'url est [https://www.youtube.com/watch?v=ueaZgk_qduw]
      La vidéo dure 13:04.

Cette interface sera sûrement en trois parties  : l’une aura la forme d’un blog, d’un moyen de visualiser temporellement la progression du bot, qui raconte ce qu’il est en train de faire et commente au fur et à mesure les particularités de son parcours. Le blog symbolise aussi une personne qui raconte son expérience personnelle, c’est donc adapté par rapport à ma thématique.
La deuxième partie sera plus visuelle, et sera une séquence des thumbnails des vidéos. C’est une autre manière de ressentir le parcours du bot, de lui donner un aspect narratif par l’image aussi.
La troisième pourra faire le lien entre les deux premières, et sera une couche plus analytique, qui fera ressortir peut-être des éléments statistiques/sociologiques sur la personnalité du bot (en fonction des tags des vidéos par exemple, en utilisant des outils tels qu’Iramuteq  ?) ou les catégories YouTube les plus présentes (Divertissement, Éducation, Sport etc.), en tout cas apporter un recul sur le parcours dans sa globalité.

V1

Pour l’instant l’interface ressemble à ça  :

PARTIE TEXTE

PARTIE IMAGES

Pour passer de l’une à l’autre pour l’instant il faut cliquer sur le bouton “IMAGES”.

La police utilisée est Happy Times at the IKOB New Game Plus Edition de Lucas Bihan. Je cherchais une police serif pour donner un côté plus “littéraire”/narratif, puisque le bot raconte son parcours. Cette police est une version contemporaine et libre de la Times New Roman.

J’ai rajouté la possibilité d’ouvrir la vidéo en question quand on clique sur le titre directement dans le post.

V2

Je voulais que sur l’interface on puisse à la fois réagir en live à l’avancement du bot, mais en même temps servir d’archive pour voir la progression dans la durée. Finalement, pour que tout ça soit plus cohérent, les deux parties ne doivent pas forcément cohabiter autant, au moins pas dans la forme qu’à la première version.

Une donnée importante du projet est le temps, puisque le bot fonctionne en temps réel, et regarde entièrement chaque vidéo (et chaque pub).
Il faudrait donc que je trouve comment faire une partie “chargement”, ou bien de trouver un moyen de marquer l’évènement “nouvelle vidéo” de façon à créer une attente si l’on reste sur la page. Le but étant de donner en vie d’attendre pour voir quelle sera la prochaine vidéo vue par le bot.

J’aimerais que l’interface ait un côté plus “littéraire/narratif”. Pour cela je veux que la partie archive ait plus l’air d’un journal intime qu’un log ou qu’une page Twitter. C’est plus ce côté “carnet” qui m’intéresse qu’un côté très numérique. J’ai trouvé les sites de monkkee et penzu, qui sont des journaux intimes en ligne, pour essayer de voir la forme que ça prend. En fait ça ressemble juste à un éditeur de texte en ligne, mais on peut personnaliser l’aspect de la page.

penzu.jpg monkkee.jpg

Pour passer de la partie live à la partie archive, j’ai pensé à un dispositif comme celui du site de Spector Books, avec un moyen de glisser avec la souris pour passer de l’un à l’autre comme si on faisait des aller-retours avec les pages d’un livre pour consulter des notes ou un index.

Pour la partie image j’ai pensé qu’au lieu de faire une séquence d’images à la verticale, où on devrait descendre en scrollant, je pourrais faire comme un flip-book. L’image suivante remplacerait la précédente lorsque l’on scrolle.

Maintenant j’utilise la police EB Garamond, une version numérisée et libre d’une Garamond, qui conserve des aspérités ou imperfections de l’impression au plomb.

En observant les gabarits de Facebook, Twitter ou YouTube j’ai remarqué qu’il y a souvent 2 ou 3 colonnes qui fonctionnent indépendamment les unes des autres (on peut scroller dans chacune d’elle séparément). La colonne du milieu est toujours là où se trouve le principal, les autres colonnes servant à afficher des éléments supplémentaires, des liens pour la navigation, des pubs ou des contenus recommandés. Je voudrais donc partir de cette configuration un peu comme base.

Partie technique

Collecte

Extension Javascript

J’ai d’abord essayé de faire une extension de navigateur pour Firefox, qui enregistre l’URL à chaque fois que la lecture automatique change de vidéo (j’ai commencé par YouTube) et qui m’envoie la liste par mail tous les 10 vidéos (les extensions n’ont pas d’accès à la possibilité d’écrire ou de lire sur un fichier local). Pour ça j’ai utilisé les Mutation Observer de javascript, qui permettent de lancer des instructions quand certaines mutations apparaissent dans le DOM, ainsi que la librairie smtp.js, qui grâce au serveur smtp de Gmail, me permet de m’envoyer des mails.
Les extensions de navigateurs fonctionnent de la même façon sur Chrome ou Firefox  :

  • le fichier manifest.json sert à régler différents paramètres. Il peut par exemple servir de filtre pour choisir les onglets ou les noms de domaines dans lesquels les scripts s’éxécutent
  • le “content-script” (ici espion.js) peut accéder au contenu du DOM de la page dans laquelle il est lancé (récupérer des infos et modifier la page en js). Il peut envoyer des informations au “background script” et vice-versa
  • le script background.js peut effectuer plus d’actions (avoir accès à des APIs ou ce genre de choses) mais ne peut pas accéder au DOM
manifest.json
{
"manifest_version": 2,
  "name": "Espion",
  "version": "1.0",
 
  "description": "Enregistre toutes les urls des vidéos lancées par l'onglet up next de Youtube et les musiques de Spotify",
 
  "background": {
    "scripts": ["background.js"]
  },
 
  "icons": {
    "48": "icons/border-48.png"
  },
 
  "content_scripts": [
    {
      "matches": ["*://*.youtube.com/*", "*://*.spotify.com/*"],
      "js": ["espion.js"]
    }
  ]
}
espion.js
///////////////////////////FONCTIONS/////////////////////////////////
function handleResponse(message) {
  console.log(`${message.response}`);
}
 
function handleError(error) {
  console.log(`Error: ${error}`);
}
 
function notifyBackgroundPage(e) {
  var sending = browser.runtime.sendMessage({
    test: e
  });
  sending.then(handleResponse, handleError);
}
 
/////////////////////SCRIPT///////////////////////////////////////////
console.log('coucou');
 
var liens = [];
var txt;
var count = 0;
 
var observer = new MutationObserver(function(mutations) {
 
  // For the sake of...observation...let's output the mutation to console to see how this all works
	liens.push(window.location.href);
  console.log('yes');
 
  if (count == 4) {
    notifyBackgroundPage(liens);
    liens = [];
    console.log(liens);
  }
 
  count = (count+1)%5;
  console.log(count);
 
});
 
// Notify me of everything!
var observerConfig = {
	childList: true,
	characterData: true,
};
 
// Node, config
var targetNode = document.getElementById('movie_player');
 
observer.observe(targetNode, observerConfig);
background.js
/* SmtpJS.com - v3.0.0 */
var Email = { send: function (a) { return new Promise(function (n, e) { a.nocache = Math.floor(1e6 * Math.random() + 1), a.Action = "Send"; 
var t = JSON.stringify(a); Email.ajaxPost("https://smtpjs.com/v3/smtpjs.aspx?", t, function (e) { n(e) }) }) }, ajaxPost: function (e, n, t) { var a = Email.createCORSRequest("POST", e); a.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), a.onload = function () { var e = a.responseText; null != t && t(e) }, a.send(n) }, ajax: function (e, n) { var t = Email.createCORSRequest("GET", e); t.onload = function () { var e = t.responseText; null != n && n(e) }, t.send() }, createCORSRequest: function (e, n) { var t = new XMLHttpRequest; return "withCredentials" in t ? t.open(e, n, !0) : "undefined" != typeof XDomainRequest ? (t = new XDomainRequest).open(e, n) : t = null, t } };
 
var i = 0;
 
function sendEmail(txt) {
	i += 1;
	Email.send({
	Host: "smtp.gmail.com",
	Username : "***************************",
	Password : "**********",
	To : "adresse@mail.destinataire",
	From : "adresse_expéditeur@gmail.com",
	Subject : "youtube" + i,
	Body : txt,
	}).then(
		message = console.log("Mail bien envoyé !")
	);
}
 
console.log('salut');
 
function handleMessage(request, sender, sendResponse) {
  console.log(request.test);
  sendResponse({response: "Bien reçu !"});
	sendEmail(request.test);
}
 
browser.runtime.onMessage.addListener(handleMessage);

Le problème est que la version de Firefox sur Raspbian n’est pas optimisée pour le Raspberry Pi, et Firefox plante assez rapidement. En essayant avec Chromium (qui est apparemment optimisé au maximum), ça marche beaucoup mieux mais ça plante aussi quand le mail s’envoie.

J’ai quand même réussi a choper le lien de 22 vidéos à la suite avant que ça plante, en bidouillant un peu.
Avec le paquet youtube-dl (sur Linux, je sais pas s’il existe ailleurs) on peut en une commande télécharger toutes les vidéos si elles sont dans un fichier texte correctement formaté.

Selenium (Python)

Je vais essayer avec Selenium, qui est un navigateur utilisable par ligne de commande, qui peut aussi s’utiliser ‘headless’, c’est-à-dire sans interface graphique. On peut l’utiliser avec Python et donc le lancer automatiquement dès que je branche le Raspberry Pi.

Pour l’instant j’arrive à me connecter à YouTube, à lancer une vidéo, à passer les pubs et détecter quand la vidéo change.
Pour ça j’ai dû utiliser une astuce pour ne pas que Chromium me détecte comme un robot (il ne me laissait pas me connecter à YouTube car il trouvait ça dangereux). Il faut lancer une commande dans selenium qui change la propriété webdriver du navigateur en “undefined”. Ensuite il faut bidouiller avec le user-agent pour pouvoir avoir le Youtube qui tourne sur un navigateur récent.

Cette étape est sûrement différente sur le RPi parce que la technique utilisée fonctionne pour Chrome/Chromium au-dessus de la version 79, et la version de Raspbian doit être 76 ou 77 je crois.

Le début de mon script ressemble à ça (je ne l’ai pas encore essayé sur le RPi)  :

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from time import sleep
from datetime import datetime
 
USERNAME = 'adresse.mail@gmail.com'
PASSWORD = 'mot_de_passe'
 
options = webdriver.ChromeOptions()
# options.add_argument('--profile-directory=Default')
# options.add_argument("--disable-plugins-discovery");
options.add_argument('--disable-extensions')
options.add_argument("--disable-blink-features=AutomationControlled");
options.add_argument("--start-maximized")
options.add_argument("--mute-audio")
 
options.add_experimental_option("excludeSwitches", ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
browser = webdriver.Chrome(options=options)
browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  "source": """
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  """
})
browser.execute_cdp_cmd("Network.enable", {})
browser.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"}})
 
wait = WebDriverWait(browser, 4)
 
###vieux user-agent
# browser.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": {"User-Agent": "browserClientA"}})
 
#-------CONNECTION À YOUTUBE---------------
 
# browser.get('https://accounts.google.com/signin/v2/identifier?service=youtube&uilel=3&passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26hl%3Den%26next%3Dhttps%253A%252F%252Fwww.youtube.com%252F&hl=en&ec=65620&flowName=GlifWebSignIn&flowEntry=ServiceLogin')
#
# emailElem = browser.find_element_by_id('identifierId')
# ###vieux user-agent
# # emailElem = browser.find_element_by_id('Email')
# emailElem.send_keys(USERNAME)
#
# elem=browser.find_element_by_xpath('//*[@id="identifierNext"]/span/span')
# ###vieux user-agent
# # elem=browser.find_element_by_id('next')
 
# elem.click()
#
# browser.implicitly_wait(4)
#
# passElem=browser.find_element_by_name('password')
# ###vieux user-agent
# # passElem=browser.find_element_by_id('Passwd')
# passElem.send_keys(PASSWORD)
# elem=browser.find_element_by_xpath('//*[@id="passwordNext"]/span/span')
# ###vieux user-agent
# # elem=browser.find_element_by_id('signIn')
# elem.click()
 
# sleep(4)
 
#-----------------LANCER LA DERNIÈRE VIDÉO ENREGISTRÉE DANS LE LOG----------------------
###choper la dernière url enregistrée dans le log pour recommencer depuis l'arrêt du programme
# browser.get('https://www.youtube.com/watch?v=HWxXeoHndgY')
browser.get('https://www.youtube.com/watch?v=Wlyq22ybsRw')
 
###cliquer sur le bouton pour lancer la vidéo
elem=browser.find_element_by_css_selector('#movie_player > div.ytp-cued-thumbnail-overlay > button')
# elem=browser.find_element_by_xpath('//*[@id="movie_player"]/div[5]/button')
elem.click()
 
 
#----------------PASSER LA PUB----------------------
def passe_pub(temps):
    try:
        elem= WebDriverWait(browser, temps).until(EC.presence_of_element_located((By.CSS_SELECTOR, "#skip-button\:17 > span > button > span")))
        elem.click()
    except:
        print('pas de pub :)')
 
    titre = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#container > h1 > yt-formatted-string")))
    print(titre.get_attribute("textContent"))
 
#----------------RÉCUPÉRER LA DURÉE DE LA VIDÉO---------------
def duree_vid():
    durée = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "span.ytp-time-duration")))
    dur = durée.get_attribute("textContent")
 
    if len(dur) <= 5:
        pt = datetime.strptime(dur,'%M:%S')
        total_seconds = pt.second + pt.minute*60
    else:
        pt = datetime.strptime(dur,'%H:%M:%S')
        total_seconds = pt.second + pt.minute*60 + pt.hour*3600
 
    ###commande pour choper et afficher l'url actuelle
    print(browser.current_url)
    return total_seconds
 
#---------------REPÉRER LE CHANGEMENT DE PAGE (CLIQUER SUR LE BOUTON UP-NEXT)------------------------
 
# def chgmt_page(time):
    # up_next = '#movie_player > div.ytp-upnext.ytp-player-content.ytp-suggestion-set > a'
    # WebDriverWait(browser, timeout=time+1).until(EC.element_to_be_clickable((By.CSS_SELECTOR, up_next)))
    # elem=browser.find_element_by_css_selector(up_next)
    # print(elem.text)
    # elem.click()
 
 
def vid():
    # passe_pub(6)
    time = duree_vid()
    print(time)
    # passe_pub(time)
    # chgmt_page(time)
    upnext = browser.find_element_by_css_selector('#dismissable > div > div.metadata.style-scope.ytd-compact-video-renderer > a')
    upnext = upnext.get_attribute("href")
    WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
    print('nouvelle vidéo !')
 
try:
    while True:
        vid()
except KeyboardInterrupt:
    raise
except:
    print('Le programme a planté :/')

Peut être que pour des plateformes comme Facebook ou Twitter je peux utiliser un système de bot (pouvant être hébergé sur le RPi quand il est en fonctionnement) plutôt qu’un protocole complexe sur Selenium  ? Naviguer entre les utilisateurs en les taggant ou en retweetant leurs posts  ? → La page Twitter en soi est un témoin de son passage et de la construction d’une personnalité, d’un avis.

Pour l’instant les pubs YouTube font tout planter, j’essaie de voir comment importer une extension dans Selenium (au moins temporairement) pour pouvoir mettre au point le truc sans les pubs.
J’ai réussi à empaqueter des extensions au format .crx pour pouvoir lancer Selenium avec cette extension (AdBlocker), mais ça ne fonctionne pas et Selenium plante tout simplement.

J’essaie de régler le problème de mon programme qui n’arrive pas à cliquer sur “Skip Ad” (peut-être un problème de sélecteur CSS/Xpath).
Ça à l’air de marcher avec ce sélecteur (pour le bouton “Skip Ad”)  :

"#skip-button\:8 > span > button"

(Même si je n’ai pas trouvé à quoi correspond le  :8)

Un autre problème apparaît maintenant (23/04/2020)  : Google a mis à jour la détection de robots/automates, et donc le moyen que j’avais trouvé pour contourner la sécurité de Youtube ne marche plus. Quand je veux me connecter, Youtube repère que j’utilise Selenium et considère que ce n’est pas sécurisé.

Sur l’image on peut voir à droite de la barre d’URL que les cookies ne sont pas activés, peut-être que le problème est que Selenium par défaut crée des sessions qui ne ne laissent pas de cookies  ?

J’ai peut-être une piste qui utiliserait les cookies de connexion, pour me connecter automatiquement sans passer par la page de Google qui détecte Selenium.

Autre solution ?

Apparemment c’est possible de créer un profil utilisateur sur Selenium qui est gardé en mémoire dans un dossier, et qui comprend les cookies (et donc les infos de connexion) ainsi que les extensions. Si j’arrive à faire fonctionner ça j’élimine le problème de la connexion et des pubs en même temps.https://stackoverflow.com/questions/15058462/how-to-save-and-load-cookies-using-python-selenium-webdriver Mais le souci reste le même  : il faut que je puisse me connecter une première fois dans la page ouverte par Selenium pour enregistrer toutes les infos. Le navigateur ne détecte plus que je suis un robot, mais il ne me laisse pas me connecter.

Cookies ?

Selenium permet de recupérer les cookies d’une page avec browser.get_cookies(), mais le problème c’est qu’il faut déjà être connecté pour récupérer les cookies de connexion.https://stackoverflow.com/questions/45417335/python-use-cookie-to-login-with-seleniumDonc il faut que je me connecte avec Selenium pour récupérer les cookies qui me permettront de me connecter sans la page de connexion avec Selenium. Puisque je ne peux pas me connecter même manuellement dans la page ouverte par Selenium, je ne peux pas récupérer les cookies de cette façon.

Pour contourner ça j’ai essayé de récupérer les cookies depuis une session manuelle de Chromium. https://github.com/dandv/convert-chrome-cookies-to-netscape-format J’arrive à avoir un fichier de cookies au format netscape, mais Selenium n’accepte que les fichiers de cookies sous forme d’objets JSON “sérialisés”, donc je cherche maintenant un moyen de conversion.
J’ai trouvé https://coockie.pro/pages/netscapetojson/ ce site (en russe) qui permet cette conversion.
Un objet sérialisé est une variable convertie sous forme de fichier ou de chaîne de caractères qui peut donc être partagée et réutilisée dans un autre script. La sérialisation c’est le processus de conversion d’un objet dans un format binaire transportable et reconstructible pour créer un clone de cet objet dans un autre programme. La désérialisation c’est l’étape de reconstruction de l’objet dans le programme.
En Python pour faire ça on utilise le module Pickle. Apparemment il n’est pas sécurisé, mais vu que je ne vais l’utiliser en local c’est pas très grave.

J’ai réussi la conversion, je copie-colle dans pickling.py les cookies au format JSON, je change quelques trucs (transformer les objets JSON ‘true’, ‘false’ et ‘null’ respectivement en ‘True’, ‘False’ et ‘None’, qui sont les équivalents en Python) et je “pickle” le résultat dans cookies.pkl  :

pickle.dump(to_pickle, open("cookies.pkl","wb"))

Le mode “wb” est pour “write binary”, puisque pickle convertit l’objet en binaire pour l’enregistrer.

On obtient bien un truc illisible :

��]q

(je ne peux pas tout copier apparemment).

Et pour “unpickle” dans mon programme principal  :

cookies = pickle.load(open("cookies.pkl", "rb"))

Quand j’importe les cookies dans Selenium ça plante (apparemment un des cookies a un mauvais nom), j’essaie de voir lequel pour corriger ça.

Petit debugging  :

for i, cookie in enumerate(cookies):
    try:
        browser.add_cookie(cookie)
    except:
        print('Le cookie %s ne marche pas' % i)

C’est le dernier cookie qui ne marche pas, j’ai sûrement copié-collé une ligne vide en trop.
J’ai aussi enlevé la première qui était juste la ligne disant que le cookie était au format Netscape.

Les cookies sont maintenant importés. Il faut sûrement recharger la page pour qu’ils prennent effet :

browser.refresh()

Et ça marche  :).

J’arrive à me connecter sur YouTube à nouveau, même si c’est moins impressionnant que de voir les formulaires qui se remplissent tous seuls.

Je vais alors essayer de créer un profil utilisateur pour inclure les extensions qui bloquent les pubs.

Passer les pubs

Je crois que j’avais simplement oublié de désactiver l’option –disable extensions, ce qui faisait planter Selenium quand je chargeais une extension. J’arrive maintenant à charger des extensions sans soucis (oups).

Petites corrections

Pour corriger le problème du dédoublage des liens dans le log (quand une nouvelle vidéo se lance, le programme détecte 6-7 changements d’URL et relance la boucle 6-7 fois, ce qui est absurde. Il suffit de rajouter sleep(1) à la fin de la boucle pour prendre en compte le chargement de la nouvelle page.

def new_vid():
    """Fonction qui lance un protocole à chaque changement de page."""
    get_infos()
    time, tps = duree_vid()
    print('      La vidéo dure %s.' % tps)  #NARRATIF
    upnext = browser.find_element_by_css_selector('#dismissable > div > div.metadata.style-scope.ytd-compact-video-renderer > a')
    upnext = upnext.get_attribute("href")
    print('Je regarde la vidéo jusqu\'au bout.')
    WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
    print('\nUne nouvelle vidéo !')  #NARRATIF
    sleep(1)

Ensuite il faut corriger le problème de la duplication du dernier lien (quand on ouvre la dernière vidéo on ré-enregistre son URL).
Au moment de l’enregistrement on vérifie que l’URL actuelle n’est pas la même que la dernière de la liste.

with open('log_urls.txt', 'a+', encoding='utf8') as f_2:
    last_vids = f_2.read()
    last_vids = last_vids.splitlines()
    if last_vids[len(last_vids)-1] != browser.current_url:
        f_2.write(browser.current_url + '\n')

Headless ?

Maintenant je veux essayer de faire tourner le programme en mode “headless” (sans interface graphique), pour que ce soit plus rapide et moins gourmand, pour pas que ça ne fasse planter le Raspberry.
Il faut juste rajouter

options.add_argument("headless")

dans les options de Selenium.

Chrome et Firefox ont tous les deux des modes headless, mais pour Chrome dans ce cas-là on ne peut pas utiliser d’extensions, et il faudrait que je passe les pubs manuellement en cliquant sur “Skip Ad”.
On peut apparemment utiliser des extensions dans Firefox, mais Firefox est mal optimisé pour le Raspberry. Je pourrais essayer mais il faut que je réfléchisse à la connotation donnée par ces choix en fonction de la personnalité de “Philippe”.

  • Avec Firefox ce serait plus facile à mettre en place, mais le choix de Firefox en tant qu’utilisateur sous-entend peut-être une personnalité qui se soucie de sa vie privée sur Internet.
  • Chromium demanderait que je fasse de la bidouille pour passer les pubs manuellement (ce qui serait plus en accord avec l’idée d’un. utilisateur.ice lambda, qui n’a pas forcément d’adblocker) mais serait plus logique (Chrome est le navigateur le plus utilisé, + de 64% des gens l’utilisent en Novembre 2019 d’après Wikipédia).

Ce n’est pas vraiment nécessaire que mon bot tourne en headless, puisque pour l’instant il va tourner chez moi.

Choix de la vidéo

Je vais le baser le choix de la première vidéo lancée par le bot sur le nombre de caractères en capitales dans le titre.

###fonction pour calculer la proportion de capitale dans une string
def nbr_uppr(string):
    uppercase = 0
    for c in string:
        if c.isupper():
            uppercase += 1
        else:
            pass
    proportion = uppercase/len(string)
    return proportion
 
###récupère tous les titres de vidéos de la page d'accueil
browser.get('https://www.youtube.com/')
titres = browser.find_elements_by_id('video-title')
        proportion_upper = []
        ###sélectionne seulement les 8 premiers titres (ceux que l'on peut voir quand on ouvre la page d'accueil)
        for r in range(0,8):
            str_titre = str(titres[r].get_attribute('textContent'))
            proportion_upper.append(nbr_uppr(str_titre))
        ###max(array) retourne la valeur la plus haute de l'array
        max_index = proportion_upper.index(max(proportion_upper))
        titres[max_index].click()

Sur le Raspberry

Maintenant que le programme fonctionne sur mon ordi, il faut le transférer sur mon ordi. J’ai essayé de refaire la même démarche que j’avais effectuée sur mon ordi, mais les chromedrivers disponibles au téléchargement sont adaptés à une architecture x64, alors que le RPi a une architecture x32. Je suis tombé sur ce thread reddit qui explique pas à pas comment faire pour avoir un chromedriver adapté au RPi (x32 et armhf). Le driver que j’ai trouvé est la version 65 et quelques, alors que Chromium est en version 78 sur le Rpi. Ça n’a pas l’air de poser de soucis pour l’instant.

Il faut maintenant que j’adapte un peu le code pour qu’il tourne sur le RPi (beaucoup plus lent, il faut rajouter des conditions pour être sûr que les éléments soient chargés avant d’essayer d’y accéder).

Le programme tourne sur le RPi, vraiment très lentement mais ça fonctionne. Malheureusement il plante au bout d’un moment. D’après mes tests parfois c’est Chromium qui crashe, parfois c’est c’est juste que la page ne charge pas bien. C’est à moitié un problème de connexion et à moitié un problème de mémoire peut-être. Je vais peut-être essayer de le faire tourner en headless.

Pour faire tourner le programme en headless il faut enlever l’extension qui permet de bloquer les pubs. Avant ça me posait problème parce qu’il fallait absolument que je passe les pubs pour récupérer la durée de la vidéo (et pas la durée de la pub), comme ça je pouvait indiquer à Selenium le temps d’attente avant qu’une nouvelle vidéo apparaisse. Mais en fait je peux mettre un temps très long (par exemple 5h) pour être sûr que le programme attende bien sagement la fin de la vidéo pour détecter le changement de vidéo, qui s’opère tout seul.

Sur mon ordi mon programme marche en headless, je n’ai plus qu’à le mettre sur mon RPi et le brancher en ethernet et c’est parti  ! Pour le faire fonctionner sur le RPi, il faut modifier un peu le script. À un moment j’utilise un bloc except sans préciser d’erreur, ce qui fait que le programme ne plante pas s’il y a un problème à ce moment-là. Et puisqu’il tourne en headless, je ne peux pas voir s’il a planté.

Je modifie donc

###cliquer sur le bouton play pour lancer la première vidéo
try:
  elem = browser.fin_elements_by_css_selector("#movie_player > div.ytp-cued-thumbnail-overlay > button")
  elem.click()
except:
  continue

par

###cliquer sur le bouton play pour lancer la première vidéo
elem = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#movie_player > div.ytp-cued-thumbnail-overlay > button")))
elem.click()

qui attend que le bouton “play” soit chargé pour cliquer dessus et lancer la vidéo.
https://serverfault.com/questions/96499/how-to-automatically-start-supervisord-on-linux-ubuntu Il me reste à trouver un moyen de “surveiller” l’activité du programme, pour le relancer automatiquement s’il plante.
Pour cela je vais utiliser supervisor, qui me permet de transformer un programme en “daemon”, c’est-à-dire en tâche fonctionnant en arrière-plan. Je peux le paramétrer pour définir le nombre de fois que supervisor doit essayer de relancer le programme s’il plante, le chemin pour des fichiers de log etc.

Puisque le programme sera lancé par un autre programme (et donc pas depuis le dossier de travail), il faut que dans le script Python tous les chemins soient noté en absolu, et pas relativement au dossier dans lequel se trouve le programme (sinon ça va planter lorsque le script cherchera les cookies ou les logs par exemple).

Dans le fichier de configuration de supervisor (/etc/supervisord.conf) je rajoute

[program:portrait-robot]
command=python3 ~/Documents/BOULOT/AP/portrait-robot/portrait_robot.py
priority=0
startsecs=50
startretries=5
stdout_logfile=~/Documents/BOULOT/AP/portrait-robot/logs/stdout_log.txt
stderr_logfile=~/Documents/BOULOT/AP/portrait-robot/logs/stdout_err.txt

qui correspondent aux paramètres énoncés plus haut.

Maintenant, il faut que je fasse en sorte que supervisor (et donc mon programme en python) se lance tout seul à chaque démarrage du RPi.
Je suis les instructions de cette page.


Après un essai concluant je me rends compte que les vidéos trouvées par le Raspberry sont toutes en anglais, qui doit être par défaut le paramètre du compte Google. Il faut que je change ça et que je recommence pour avoir des vidéos en français.
J’ai changé les paramètres du compte Google et refait les manipulations pour obtenir les cookies, et cette fois-ci effectivement j’ai des résultats en français sur la page d’accueil.

Interface

Structure

Essai avec javascript

Je suis en train de construire une interface en PHP/javascript pour extraire les urls du log généré par le programme, et en extraire des informations (titre, durée, chaîne, thumbnails etc.) grâce à l’API de YouTube pour pouvoir les mettre en forme dynamiquement. ces informations me serviront à faire une “analyse” du contenu au moment où je le collecte. Je pense qu’une partie prendra une forme textuelle, comme une narration par le bot des actions effectuées (le log du programme python fait ça en partie, mais de manière très scriptée).
Le but est que le bot commente par des phrases du style “Hmmm j’en ai marre celle-là ça fait X fois que je tombe dessus”, ou alors “J’aime bien les longues vidéos” si il y a 3 fois d’affilée des vidéos de plus de 2h, ce genre de choses.

Je récupère le contenu du fichier texte et l’affiche ligne par ligne dans des balises p  :

  $fh = fopen('log_urls.txt','r');
    while ($line = fgets($fh)) {
      echo('<p class="url">'.$line.'</p>');
    }
    fclose($fh);

Je rends ces balises invisibles  :

.url{
  display:none;
}

Je récupère le contenu (présent bien qu’invisible) dans une array  :

var urls_collection = document.getElementsByClassName('url');
urls_array = Array.from(urls_collection);
urls = [];
//faire des éléments HTML récupérés une array d'urls
for (url of urls_array){
  //il faut enlever le caractère "\n" à la fin de chaque ligne
  strippedUrl = url.textContent.trim()
  urls.push(strippedUrl);
}

Je fais appel à l’API de YouTube avec une librairie javascript  :

function loadClient() {
  gapi.client.setApiKey(apiKey);
  return gapi.client.load("https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest")
    .then(function() {
        console.log("GAPI client loaded for API");
      },
      function(err) {
        console.error("Error loading GAPI client for API", err);
      });
}
// Make sure the client is loaded and sign-in is complete before calling this method
function execute(url) {
  vid_id = url.replace('https://www.youtube.com/watch?v=', '');
  return gapi.client.youtube.videos.list({
      "part": "snippet",
      "id": vid_id
    })
    .then(function(response) {
        // Handle the results here (response.result has the parsed body).
        result = response.result.items['0'].snippet.title;
        // console.log('TITRE : '+result);
        // console.log(typeof ''+result);
        console.log(result)
        return {
          result:result
        };
      },
      function(err) {
        console.error("Execute error", err);
      });
}

Il faut aussi charger auparavant la librairie dans la page HTML/PHP.

 <script src="https://apis.google.com/js/api.js" type="text/javascript"></script>

Finalement, cette technique est compliquée puisqu’elle implique des histoires de code asynchrone, et de trouver un moyen pour être sûr que les actions de javascript (appels à l’API) se fassent dans le bon ordre, sinon ça ne fonctionne pas. En plus, cela voudrait dire qu’à chaque chargement de la page il y a autant de requêtes à l’API qu’il y a de vidéos collectées par le bot, ce qui va vite devenir ingérable (et très mal optimisé). Pour finir, si une vidéo est supprimée au moment de la consultation de la page, l’API ne pourra pas la trouver.

Appeler l'API avec Python

Le moyen le plus fiable/efficace est donc de récupérer les infos sur la vidéo (titre, durée, date de publication, description, commentaires, etc.) toujours avec l’API mais via Python/Selenium. De cette façon, on ne fait une requête à l’API qu’une fois par jour, l’interface Python est plus compréhensible pour moi, et je peux enregistrer le résultat au format JSON (qui sera très pratique à traiter en javascript ou en PHP).
J’obtiendrais donc un log en JSON sur le Raspberry, qui se remplira petit à petit. Ensuite il suffit que le Raspberry envoie sur le serveur au fur et à mesure le contenu du log, et ainsi je peux mettre en page dynamiquement ce contenu grâce à javascript et PHP.

À la place de simplement enregistrer l’URL courante dans un fichier texte, je rajoute cette partie dans mon programme principal en Python (le script qui utilise Selenium)  :

import json
import googleapiclient.discovery
import googleapiclient.errors
 
with open('API.txt', 'r', encoding="utf-8") as f:
    api_key = f.read().rstrip()
 
#authentification et configuration de l'API
api_service_name = "youtube"
api_version = "v3"
max_results = 1
 
with open('API.txt', 'r', encoding="utf-8") as f:
    DEVELOPER_KEY = f.read().rstrip()
 
youtube = googleapiclient.discovery.build(
    api_service_name, api_version, developerKey=DEVELOPER_KEY)
 
#la requête à l'API en elle-même
request = youtube.videos().list(
    part="snippet,contentDetails,statistics",
    id="L_LUpnjgPso",
)
response = request.execute()['items'][0]
 
###supprimer les éléments inutiles pour alléger le fichier au maximum
supp = ['kind', 'etag', 'id']
for s in supp:
    del response[s]
 
supp2 = ['channelId', 'liveBroadcastContent', 'localized']
for s2 in supp2:
    del response['snippet'][s2]
 
supp3 = ['dimension', 'projection', 'contentRating', 'licensedContent']
for s3 in supp3:
    del response['contentDetails'][s3]
 
###trouver le thumbnail avec les plus grande définition pour chaque url
thumb_sizes = ['maxres', 'high', 'medium', 'standard', 'default']
for thumb in thumb_sizes:
    if thumb in response['snippet']['thumbnails']:
        ###on utilise la liste temp_list car on ne peut pas supprimer des éléments du dictionnaire directement pendant son itération
        temp_list = []
        for element in response['snippet']['thumbnails']:
            if thumb != element:
                temp_list.append(element)
        for ar in temp_list:
            del response['snippet']['thumbnails'][ar]
        break
 
###changer le categoryId par une chaîne de caractère compréhensible
with open('catégoriesVidéos.json', 'r', encoding='utf-8') as f:
    data = f.read()
    video_categories = json.loads(data)['items']
 
    for cat in video_categories:
        if response['snippet']['categoryId'] == cat['id']:
            response['snippet']['categoryId'] = cat['title']
 
###simplifier un peu le format de la durée de la vidéo
durée_simple = response['contentDetails']['duration'].lstrip('PT').lower()
response['contentDetails']['duration'] = durée_simple
 
###enregistrer le tout dans un fichier JSON
with open('result_API.json', 'a', encoding="utf8") as fp:
    json.dump(response, fp)
    fp.write('\n')

J’ai essayé de nettoyer au maximum le fichier JSON pour que ce soit le plus rapide possible lorsque le RPi va envoyer des données sur le serveur. Vu que je suis plus à l’aise en Python ça me permet aussi de ne pas galérer en javascript ou en PHP pour faire la même chose.

J’ai utilise cette fonction de l'API de YouTube pour récupérer le fichier catégoriesVidéos.json, qui me permet d’avoir la catégorie de la vidéo en français, au lieu d’un code à 2 chiffres.
J’ai quand même nettoyé un peu le fichier JSON pour avoir quelque chose de plus clair, et plus simple pour travailler.

Il ressemble à ça  :

{
  "items": [{
      "id": "1",
      "title": "Films et animations"
    },
    {
      "id": "2",
      "title": "Auto/Moto"
    },
    {
      "id": "10",
      "title": "Musique"
    },
    {
      "id": "15",
      "title": "Animaux"
    },
    {
      "id": "17",
      "title": "Sport"
    },

On obtient alors pour chaque vidéo (à partir d’une url) ce genre d’objet JSON  :

{
"snippet": 
  {"publishedAt": 
  "2016-10-02T14:05:46Z", 
  "title": "Fireplace 10 hours full HD", 
  "description": "Fireplace 10 hours full HD for a romantic moment.\n10 hours burning logs loop play.", 
  "thumbnails": 
    {"maxres": 
      {"url": "https://i.ytimg.com/vi/L_LUpnjgPso/maxresdefault.jpg", 
      "width": 1280,
      "height": 720}}, 
  "channelTitle": "Fireplace 10 hours", 
  "tags": 
    ["fireplace 10 hours", 
    "fireplaces", 
    "fireplace video hd", 
    "fireplace video", 
    "HD fireplace", 
    "fireplace burning", 
    "burning logs",   
    "fireplace"], 
    "categoryId": "Divertissement"}, 
"contentDetails": 
  {"duration": "10h1m26s", 
  "definition": "hd", 
  "caption": "false"}, 
"statistics": 
  {"viewCount": "21852887", 
  "likeCount": "55694", 
  "dislikeCount": "4133", 
  "favoriteCount": "0"
}}

Peut-être que toutes ces informations ne me serviront pas et que j’en enlèverais plus tard, pour réduire encore la taille des fichiers envoyés.

Affichage dans la page PHP

Pour afficher ces informations j’utilise PHP afin de créer une structure HTML  :

    <?php
      $arr_JSON = [];
      $fh = fopen('/chemin/log_results.json', 'r');
      while ($line = fgets($fh)) {
        array_push($arr_JSON, json_decode($line, TRUE));
       }
      fclose($fh);
 
      //on inverse l'ordre de l'array pour afficher les dernières vidéos en haut de la page (comme un blog)
      $arr_JSON = array_reverse($arr_JSON);
      $thumb_res = ['maxres', 'high', 'medium', 'default'];
      foreach($arr_JSON as $JSON) {
        echo '<article class="post">';
          echo '<aside class="date">'.$JSON['datetime']['heure'].''.$JSON['datetime']['jour'].'</aside>';
          echo '<div class="vid">';
            echo '<p>
                  Je regarde une nouvelle vidéo. 
                  Le titre est <span class="titre">'.$JSON['snippet']['title'].'.</span>
                  Elle dure <span class="durée">'.$JSON['contentDetails']['duration'].'.</span>
                  </p>';
                  //trouve l'image dans la plus grande résolution possible
                  foreach ($thumb_res as $res) {
                    if (array_key_exists($res, $JSON['snippet']['thumbnails'])){
                      echo '<img class="hidden" src="'.$JSON['snippet']['thumbnails'][$res]['url'].'" alt="'.$res.'_img">';
                      break;
                    } else {
                      continue;
                    }
                  }
          echo '</div>';
        echo '</article>';
      }
    ?>

Sur chaque ligne du fichier JSON se trouvent les informations concernant une vidéo. Chacune de ces vidéos fera l’objet d’un “post” comme celui-ci  :

900 Il contient des infos basiques (titre, durée, date à laquelle le bot l’a regardée).

Il faut désormais que je me penche sur la façon dont je vais analyser les métadonnées de chacune de ces vidéos pour les faire paraître dans l’interface.

Cependant toutes les métadonnées n’existent pas forcément pour chaque vidéo (certaines n’ont pas de tags par exemple)  : il faut donc vérifier leur existence dans le tableau associatif crée à partir du log en JSON pour chaque vidéo. En PHP il y a  deux manières de faire ça, et je vais donc utiliser isset() puisque je n’aurais pas de cas où le contenu de l’array serait “null”.

  if (isset($JSON['snippet']['tags'])){
    foreach($JSON['snippet']['tags'] as $tag){
      echo '<p>'.$tag;'<p>';
    }
  }

J’ai fait un bout de code qui permet de comparer tous les tags dans les x derneiers éléments pour ressortir le plus récurrent et le poster dans le blog. Ça permet de se rendre compte périodiquement des récurrences ou des thématiques qui ressortent à l’échelle “locale” du blog.

  //s'il y a des tags, on les range dans une array pour les comparer
        if (isset($JSON['snippet']['tags'])){
          foreach ($JSON['snippet']['tags'] as $tag){
            array_push($compare_tags, $tag);
          }
        }
 
        if ($compteur%4 == 3){
          //compare les tags des dernières vidéos et en sort le plus récurrent
          $compare_tags = array_map('strtolower', $compare_tags);
          $compare_tags = array_count_values($compare_tags);
          arsort($compare_tags);
          $sorted_tags = array_keys($compare_tags);
          $sorted_keys = array_keys(array_flip($compare_tags));
          //affiche le tag le plus récurrent si il y a plus d'une occurence (et donc si c'est un minimum pertinent)
          if ($sorted_keys[0] > 1){
            echo '<article class="vid">';
            echo 'En ce moment j\'aime bien '.$sorted_tags[0].' !';
            echo '</article>';
            //vide l'array, permet de comparer seulement les x derniers éléments
            $compare_tags = [];
          }
        }
        echo $compteur%4;
        $compteur += 1;

Cette structure est applicable à plusieurs métadonnées, avec des échelles différentes  : on peut choisir de comparer les durées des vidéos, et d’en faire un compte-rendu avec une période de 20 vidéos, avec les catégories toutes les 30 vidéos, ou encore avec le nombre de likes ou de vues tous les 5 vidéos, etc.

Ces périodes peuvent être variables pour amener un peu de souplesse  : on pourrait écrire ça comme ça :

if ($compteur%15 == mt_rand(13, 15){
 //code    
}

La fonction mt_rand est apparemment plus rapide et "mieux aléatoire" que la fonction rand().

Le problème est que ces posts ne sont pas dans l’ordre que je souhaiterais. J’ai utilisé une astuce dans la boucle en inversant l’ordre de l’array, c’est-à-dire que la dernière vidéo vue est postée en premier dans le code HTML, ce qui la place en haut de la page. Le problème, c’est que l’analyse comparative des tags par exemple se fait après dans cette même boucle, et donc si je décide de poster un commentaire du style “En ce moment j’aime bien x sujet”, celui-ci se retrouvera en 3e position en partant du haut de la page. Le sens d’exécution du code/d’affichage est l’inverse du sens de logique de post sur un blog (les actualités les plus récentes en haut de la navigation.).

Je dois rectifier cet ordre soit par du javascript qui vient déplacer le noeud au bon endroit du DOM, soit peut-être par des paramètres de flexbox-order un peu compliqués pour que le noeud crée après soit positionné avant.

En fait c’est très simple avec les flexbox  ! Je peux enlever la ligne qui inverse l’ordre de l’array dans le code PHP.

Avec ce code CSS, les éléments générés dans l’ordre peuvent être affichés dans l’ordre inverse :

/*Cet élément HTML est le conteneur de tous les posts, le changement d'ordre s'applique à tous ses enfants*/
section#corps{
  display:flex;
  flex-direction: column-reverse;
}
Affichage v2

J’utilise des spans au lieu des divs pour faire en sorte que tous les messages se suivent.

  foreach($arr_JSON as $JSON) {
        echo '<div class="post hideable">';
          //afficher la date
          echo '<aside class="date">'.$JSON['datetime']['jour'].''.$JSON['datetime']['heure'].'</aside>';
          echo '<p>';
          echo '<span class="text">';
            echo 'Je regarde une nouvelle vidéo. Le titre est <a href="https://www.youtube.com/watch?v='.$JSON['id'].'" class="titre" target="_blank">'.$JSON['snippet']['title'].'</a>.
            Elle dure <span class="durée">'.$JSON['contentDetails']['duration'].'. </span> ';
          echo '</span>';

Pour les images je crée une autre section  :

 //on affiche les images dans une section différente donc on refait une boucle
 foreach($arr_JSON as $images_JSON) {
   //trouver l'image dans la plus grande résolution possible et l'afficher
   foreach ($thumb_res as $res) {
     if (array_key_exists($res, $images_JSON['snippet']['thumbnails'])){
       echo '<div>
             <img class="hidden" src="'.$images_JSON['snippet']['thumbnails'][$res]['url'].'" alt="'.$res.'_img">
             </div>';
       break;
     } else {
       continue;
     }
   }
 }
 

Avec l’event “wheel” de javascript on peut récupérer n’importe quel mouvement de scroll, même s’il n’y a rien à scroller (alors que l’évènement “scroll” ne s’active que lorsque l’on doit défiler pour voir le reste du contenu).
Le delta Y de cet évènement permet de savoir quand on va vers le haut ou vers le bas.

imgs[0].classList.toggle('hidden');
var number = 0;
var prev_number = 0;
window.addEventListener('wheel', function(event) {
  if (event.deltaY < 0) {
    //scrolling up
    prev_number = number;
    number = clamp(number - 1, 0, imgs.length - 1);
  } else if (event.deltaY > 0) {
    //scrolling down
    prev_number = number;
    number = clamp(number + 1, 0, imgs.length - 1);
  }
  console.log('number', number);
  imgs[number].style.display = 'inherit';
  if (number != prev_number) {
    imgs[prev_number].style.display = 'none';
  }
});

Et la fonction clamp, qui permet de restreindre un nombre entre deux valeurs (pour éviter que des indexs trop grands ou trop petits créent une IndexError).

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

J’essaie de compartimenter les différentes parties de l’interface (LIVE | TEXTE | IMAGES) en colonnes dont la taille peut être ajustée, afin de pouvoir se concentrer sur une partie en particulier par exemple. Pour cela j’utilise la librairie split.js.

PARTIE JS  :

//#live et #texte sont pour l'instant les deux colonnes que l'on peut retailler
Split(['#live', '#texte'], {
  sizes: [20, 80],
  minSize: 100,
  gutterSize:2,
});

PARTIE CSS  : Les colonnes retaillables ont la classe “split”, et la gouttière (“gutter”, “gutter-horizontal”) est crée automatiquement entre les colonnes par la librairie. On peut changer la couleur ou mettre une image pour décorer cette gouttière.
Apparemment il faut que les colonnes aient une hauteur définie pour que la librairie fonctionne. Pour l’instant je leur ai donné une hauteur fixe car le reste ne fonctionnait pas.

.gutter {
    background-color: #eee;
    height:100%;
    top:4em;
}
 
.gutter.gutter-horizontal {
  float: left;
  background-color: black;
  cursor: ew-resize;
}
 
.split, .gutter.gutter-horizontal {
  height: 600px;
  overflow-x: hidden;
  overflow-y: auto;
}

J’utilise ensuite AJAX pour récupérer le dernier log avec javascript, ce qui me permet d’utiliser les valeurs de l’objet JSON pour calculer le temps d’attente pour la prochaine vidéo.

let requestURL = 'http://curlybraces.be/erg/2019-2020/portrait-robot/log_last.json';
let request = new XMLHttpRequest();
request.open('GET', requestURL);
request.responseType = 'json';
request.send();
 
var logLast;
request.onload = function() {
  logLast = request.response;
  document.getElementById('test').innerHTML = 'Prochaine vidéo dans '+logLast.contentDetails.duration;
};
portrait-robot.txt · Dernière modification: 07/12/2020 01:00 de thomas