add mpv youtube et twitch
This commit is contained in:
parent
481556c7ac
commit
35b0a52424
12 changed files with 650 additions and 19 deletions
487
dotfiles/mpv/feeds.lua
Normal file
487
dotfiles/mpv/feeds.lua
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
--[[
|
||||
feeds.lua — menu "Abonnements YouTube / Twitch" pour mpv, rendu via uosc.
|
||||
|
||||
Lit deux CSV :
|
||||
- le subscriptions.csv de Google Takeout
|
||||
(col. 1 = ID de chaîne, col. 3 = nom de la chaîne)
|
||||
- un twitch.csv maison (col. 1 = login Twitch, col. 2 = libellé, optionnelle)
|
||||
|
||||
Trois sources :
|
||||
- Abonnements YouTube : dernières vidéos, via les flux RSS publics (curl)
|
||||
- Parcourir une chaîne : catalogue d'une chaîne, via yt-dlp
|
||||
- Twitch : chaînes suivies, lecture via streamlink
|
||||
|
||||
Aucune clé API n'est nécessaire.
|
||||
Dépendances : uosc, curl, yt-dlp, streamlink.
|
||||
Raccourci par défaut : alt+h
|
||||
--]]
|
||||
|
||||
local utils = require("mp.utils")
|
||||
local options = require("mp.options")
|
||||
local msg = require("mp.msg")
|
||||
|
||||
local o = {
|
||||
youtube_csv = "~/nixos-config/dotfiles/mpv/youtube.csv",
|
||||
twitch_csv = "~/nixos-config/dotfiles/mpv/twitch.csv",
|
||||
key = "alt+h",
|
||||
per_channel = 3, -- nb max de vidéos gardées par chaîne (flux RSS)
|
||||
max_videos = 150, -- taille max de la liste des abonnements
|
||||
channel_videos = 1000, -- nb de vidéos chargées en parcourant une chaîne
|
||||
twitch_live_check = true, -- détection "en direct" (best-effort, sans clé API)
|
||||
}
|
||||
options.read_options(o, "feeds")
|
||||
|
||||
local cache = { youtube = nil, twitch = nil, channels = {} }
|
||||
|
||||
local function expand(path)
|
||||
return mp.command_native({ "expand-path", path })
|
||||
end
|
||||
|
||||
local function unescape(s)
|
||||
if not s then
|
||||
return ""
|
||||
end
|
||||
s = s:gsub("<", "<"):gsub(">", ">"):gsub(""", '"')
|
||||
s = s:gsub("'", "'"):gsub("'", "'"):gsub("&", "&")
|
||||
return s
|
||||
end
|
||||
|
||||
local function trim(s)
|
||||
return (s or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
local function format_duration(sec)
|
||||
if type(sec) ~= "number" or sec <= 0 then
|
||||
return ""
|
||||
end
|
||||
sec = math.floor(sec)
|
||||
local h = math.floor(sec / 3600)
|
||||
local m = math.floor((sec % 3600) / 60)
|
||||
local s = sec % 60
|
||||
if h > 0 then
|
||||
return string.format("%d:%02d:%02d", h, m, s)
|
||||
end
|
||||
return string.format("%d:%02d", m, s)
|
||||
end
|
||||
|
||||
local function open_menu(m)
|
||||
mp.commandv("script-message-to", "uosc", "open-menu", utils.format_json(m))
|
||||
end
|
||||
|
||||
local function update_menu(m)
|
||||
mp.commandv("script-message-to", "uosc", "update-menu", utils.format_json(m))
|
||||
end
|
||||
|
||||
local function loading_menu(type_, title)
|
||||
open_menu({
|
||||
type = type_,
|
||||
title = title,
|
||||
items = { { title = "Chargement…", icon = "spinner", selectable = false, keep_open = true } },
|
||||
})
|
||||
end
|
||||
|
||||
-- Renvoie les lignes du fichier, débarrassées d'un éventuel \r final (les
|
||||
-- exports Takeout peuvent être en CRLF).
|
||||
local function read_lines(path)
|
||||
local lines = {}
|
||||
local full = expand(path)
|
||||
local f = io.open(full, "r")
|
||||
if not f then
|
||||
msg.warn("CSV illisible : " .. tostring(full))
|
||||
return lines
|
||||
end
|
||||
for line in f:lines() do
|
||||
lines[#lines + 1] = (line:gsub("\r$", ""))
|
||||
end
|
||||
f:close()
|
||||
return lines
|
||||
end
|
||||
|
||||
--------------------------------------------------- Lecture
|
||||
|
||||
mp.register_script_message("feeds-play", function(url)
|
||||
mp.commandv("loadfile", url, "replace")
|
||||
end)
|
||||
|
||||
mp.register_script_message("feeds-twitch", function(login)
|
||||
mp.osd_message("Ouverture de " .. login .. " via streamlink…", 3)
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = { "streamlink", "--player=mpv", "--twitch-low-latency", "twitch.tv/" .. login, "best" },
|
||||
detach = true,
|
||||
-- sans ceci, mpv tue le processus dès qu'aucune vidéo n'est en lecture
|
||||
playback_only = false,
|
||||
}, function() end)
|
||||
end)
|
||||
|
||||
--------------------------------------------------- CSV YouTube
|
||||
|
||||
-- Format Takeout : ID,URL,Titre — l'URL ne contient jamais de virgule, donc
|
||||
-- tout ce qui suit la 2e virgule est le titre (éventuellement entre
|
||||
-- guillemets s'il contient lui-même une virgule).
|
||||
local function read_youtube_channels()
|
||||
local channels = {}
|
||||
for _, line in ipairs(read_lines(o.youtube_csv)) do
|
||||
local id = line:match("^([%w_-]+)")
|
||||
if id and id:match("^UC") then -- écarte l'en-tête du CSV
|
||||
local title = line:match("^[^,]*,[^,]*,(.*)$") or ""
|
||||
title = trim(title)
|
||||
local inner = title:match('^"(.*)"$')
|
||||
if inner then
|
||||
title = inner:gsub('""', '"')
|
||||
end
|
||||
if title == "" then
|
||||
title = id
|
||||
end
|
||||
channels[#channels + 1] = { id = id, title = title }
|
||||
end
|
||||
end
|
||||
table.sort(channels, function(a, b)
|
||||
return a.title:lower() < b.title:lower()
|
||||
end)
|
||||
return channels
|
||||
end
|
||||
|
||||
--------------------------------------------------- Abonnements (RSS)
|
||||
|
||||
local function youtube_menu_data(videos)
|
||||
local items = {}
|
||||
for _, v in ipairs(videos) do
|
||||
items[#items + 1] = {
|
||||
title = v.author .. " — " .. v.title,
|
||||
hint = v.published:sub(1, 10),
|
||||
value = { "script-message", "feeds-play", v.url },
|
||||
}
|
||||
end
|
||||
if #items == 0 then
|
||||
items = {
|
||||
{
|
||||
title = "Aucune vidéo récupérée — voir les logs (--msg-level=feeds=info)",
|
||||
selectable = false,
|
||||
},
|
||||
}
|
||||
end
|
||||
return { type = "yt_feed", title = "Abonnements YouTube", items = items }
|
||||
end
|
||||
|
||||
local function parse_feeds(blob)
|
||||
local videos, seen_count = {}, {}
|
||||
for entry in blob:gmatch("<entry>(.-)</entry>") do
|
||||
local id = entry:match("<yt:videoId>(.-)</yt:videoId>")
|
||||
local title = entry:match("<title>(.-)</title>")
|
||||
local author = entry:match("<author>.-<name>(.-)</name>")
|
||||
local published = entry:match("<published>(.-)</published>")
|
||||
if id and title and published then
|
||||
videos[#videos + 1] = {
|
||||
url = "https://www.youtube.com/watch?v=" .. id,
|
||||
title = unescape(title),
|
||||
author = unescape(author or "?"),
|
||||
published = published,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- les dates ISO 8601 se trient correctement comme de simples chaînes
|
||||
table.sort(videos, function(a, b)
|
||||
return a.published > b.published
|
||||
end)
|
||||
|
||||
local kept = {}
|
||||
for _, v in ipairs(videos) do
|
||||
local n = (seen_count[v.author] or 0)
|
||||
if n < o.per_channel then
|
||||
seen_count[v.author] = n + 1
|
||||
kept[#kept + 1] = v
|
||||
if #kept >= o.max_videos then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return kept
|
||||
end
|
||||
|
||||
local function youtube_menu(force)
|
||||
if cache.youtube and not force then
|
||||
open_menu(youtube_menu_data(cache.youtube))
|
||||
return
|
||||
end
|
||||
|
||||
local channels = read_youtube_channels()
|
||||
local args = { "curl", "-s", "--parallel", "--parallel-max", "16", "-m", "25" }
|
||||
for _, c in ipairs(channels) do
|
||||
args[#args + 1] = "https://www.youtube.com/feeds/videos.xml?channel_id=" .. c.id
|
||||
end
|
||||
|
||||
if #channels == 0 then
|
||||
open_menu({
|
||||
type = "yt_feed",
|
||||
title = "Abonnements YouTube",
|
||||
items = { { title = "Aucune chaîne lue dans " .. o.youtube_csv, selectable = false } },
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
msg.info(#channels .. " chaînes à interroger")
|
||||
loading_menu("yt_feed", "Abonnements YouTube")
|
||||
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = args,
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
}, function(_, res)
|
||||
local videos = {}
|
||||
if res and res.stdout and #res.stdout > 0 then
|
||||
videos = parse_feeds(res.stdout)
|
||||
end
|
||||
msg.info(
|
||||
string.format(
|
||||
"curl status=%s, %d octets, %d vidéos",
|
||||
tostring(res and res.status),
|
||||
res and res.stdout and #res.stdout or 0,
|
||||
#videos
|
||||
)
|
||||
)
|
||||
cache.youtube = videos
|
||||
update_menu(youtube_menu_data(videos))
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------- Parcourir une chaîne
|
||||
|
||||
local function channel_videos_menu_data(name, videos)
|
||||
local items = {}
|
||||
for _, v in ipairs(videos) do
|
||||
items[#items + 1] = {
|
||||
title = v.title,
|
||||
hint = v.duration,
|
||||
value = { "script-message", "feeds-play", v.url },
|
||||
}
|
||||
end
|
||||
if #items == 0 then
|
||||
items = { { title = "Rien récupéré — voir les logs (--msg-level=feeds=info)", selectable = false } }
|
||||
end
|
||||
return { type = "yt_channel", title = name, items = items }
|
||||
end
|
||||
|
||||
local function channel_videos_menu(id, name)
|
||||
if cache.channels[id] then
|
||||
open_menu(channel_videos_menu_data(name, cache.channels[id]))
|
||||
return
|
||||
end
|
||||
|
||||
loading_menu("yt_channel", name)
|
||||
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = {
|
||||
"yt-dlp",
|
||||
"--flat-playlist",
|
||||
"--dump-single-json",
|
||||
"--no-warnings",
|
||||
"--playlist-items",
|
||||
"1:" .. tostring(o.channel_videos),
|
||||
"https://www.youtube.com/channel/" .. id .. "/videos",
|
||||
},
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
}, function(_, res)
|
||||
local videos = {}
|
||||
if res and res.stdout and #res.stdout > 0 then
|
||||
local ok, data = pcall(utils.parse_json, res.stdout)
|
||||
if ok and data and data.entries then
|
||||
for _, e in ipairs(data.entries) do
|
||||
if e.id then
|
||||
videos[#videos + 1] = {
|
||||
title = e.title or e.id,
|
||||
duration = format_duration(e.duration),
|
||||
url = "https://www.youtube.com/watch?v=" .. e.id,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
msg.info(
|
||||
string.format(
|
||||
"yt-dlp status=%s, %d octets, %d vidéos (%s)",
|
||||
tostring(res and res.status),
|
||||
res and res.stdout and #res.stdout or 0,
|
||||
#videos,
|
||||
name
|
||||
)
|
||||
)
|
||||
cache.channels[id] = videos
|
||||
update_menu(channel_videos_menu_data(name, videos))
|
||||
end)
|
||||
end
|
||||
|
||||
mp.register_script_message("feeds-channel", function(id, name)
|
||||
channel_videos_menu(id, name)
|
||||
end)
|
||||
|
||||
local function channels_menu()
|
||||
local channels = read_youtube_channels()
|
||||
local items = {}
|
||||
for _, c in ipairs(channels) do
|
||||
items[#items + 1] = {
|
||||
title = c.title,
|
||||
value = { "script-message", "feeds-channel", c.id, c.title },
|
||||
}
|
||||
end
|
||||
if #items == 0 then
|
||||
items = { { title = "Aucune chaîne lue dans " .. o.youtube_csv, selectable = false } }
|
||||
end
|
||||
-- tape simplement quelques lettres pour filtrer la liste
|
||||
open_menu({ type = "yt_channels", title = "Parcourir une chaîne", items = items })
|
||||
end
|
||||
|
||||
--------------------------------------------------- Twitch
|
||||
|
||||
local function twitch_menu_data(channels)
|
||||
local items = {}
|
||||
for _, c in ipairs(channels) do
|
||||
local mark = ""
|
||||
if c.live == true then
|
||||
mark = "🔴 "
|
||||
elseif c.live == false then
|
||||
mark = "· "
|
||||
end
|
||||
items[#items + 1] = {
|
||||
title = mark .. (c.label ~= "" and c.label or c.login),
|
||||
hint = c.live == true and "en direct" or nil,
|
||||
value = { "script-message", "feeds-twitch", c.login },
|
||||
}
|
||||
end
|
||||
if #items == 0 then
|
||||
items = { { title = "Aucune chaîne lue dans " .. o.twitch_csv, selectable = false } }
|
||||
end
|
||||
return { type = "twitch_feed", title = "Twitch", items = items }
|
||||
end
|
||||
|
||||
local function twitch_menu(force)
|
||||
if cache.twitch and not force then
|
||||
open_menu(twitch_menu_data(cache.twitch))
|
||||
return
|
||||
end
|
||||
|
||||
local channels = {}
|
||||
for _, line in ipairs(read_lines(o.twitch_csv)) do
|
||||
local login = line:match("^([%w_][%w_]*)")
|
||||
if login and login:lower() ~= "login" then -- ignore l'en-tête éventuel
|
||||
local label = trim(line:match("^[^,]*,(.*)$") or "")
|
||||
channels[#channels + 1] = { login = login, label = label, live = nil }
|
||||
end
|
||||
end
|
||||
|
||||
if #channels == 0 or not o.twitch_live_check then
|
||||
cache.twitch = channels
|
||||
open_menu(twitch_menu_data(channels))
|
||||
return
|
||||
end
|
||||
|
||||
loading_menu("twitch_feed", "Twitch")
|
||||
|
||||
local logins = {}
|
||||
for _, c in ipairs(channels) do
|
||||
logins[#logins + 1] = c.login
|
||||
end
|
||||
|
||||
local script = [[
|
||||
for c in ]] .. table.concat(logins, " ") .. [[; do
|
||||
( if curl -sL -m 8 "https://www.twitch.tv/$c" | grep -q '"isLiveBroadcast":true'; then
|
||||
echo "$c live"
|
||||
else
|
||||
echo "$c off"
|
||||
fi ) &
|
||||
done
|
||||
wait
|
||||
]]
|
||||
|
||||
mp.command_native_async({
|
||||
name = "subprocess",
|
||||
args = { "sh", "-c", script },
|
||||
capture_stdout = true,
|
||||
playback_only = false,
|
||||
}, function(_, res)
|
||||
local nb_live = 0
|
||||
if res and res.stdout and #res.stdout > 0 then
|
||||
local status = {}
|
||||
for login, state in res.stdout:gmatch("(%S+)%s+(%S+)") do
|
||||
status[login] = (state == "live")
|
||||
end
|
||||
for _, c in ipairs(channels) do
|
||||
if status[c.login] ~= nil then
|
||||
c.live = status[c.login]
|
||||
if c.live then
|
||||
nb_live = nb_live + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(channels, function(a, b)
|
||||
if a.live ~= b.live then
|
||||
return a.live == true
|
||||
end
|
||||
return a.login < b.login
|
||||
end)
|
||||
end
|
||||
msg.info(
|
||||
string.format(
|
||||
"twitch status=%s, %d chaînes, %d en direct",
|
||||
tostring(res and res.status),
|
||||
#channels,
|
||||
nb_live
|
||||
)
|
||||
)
|
||||
cache.twitch = channels
|
||||
update_menu(twitch_menu_data(channels))
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------- Menu racine
|
||||
|
||||
mp.register_script_message("feeds-open", function(which, force)
|
||||
local refresh = (force == "refresh")
|
||||
if which == "youtube" then
|
||||
youtube_menu(refresh)
|
||||
elseif which == "channels" then
|
||||
channels_menu()
|
||||
elseif which == "twitch" then
|
||||
twitch_menu(refresh)
|
||||
end
|
||||
end)
|
||||
|
||||
mp.add_key_binding(o.key, "feeds-menu", function()
|
||||
open_menu({
|
||||
type = "feeds",
|
||||
title = "Chaînes",
|
||||
items = {
|
||||
{
|
||||
title = "Abonnements YouTube",
|
||||
icon = "subscriptions",
|
||||
value = { "script-message", "feeds-open", "youtube" },
|
||||
},
|
||||
{
|
||||
title = "Parcourir une chaîne",
|
||||
icon = "video_library",
|
||||
value = { "script-message", "feeds-open", "channels" },
|
||||
},
|
||||
{
|
||||
title = "Twitch",
|
||||
icon = "live_tv",
|
||||
value = { "script-message", "feeds-open", "twitch" },
|
||||
},
|
||||
{
|
||||
title = "Rafraîchir YouTube",
|
||||
icon = "refresh",
|
||||
muted = true,
|
||||
value = { "script-message", "feeds-open", "youtube", "refresh" },
|
||||
},
|
||||
{
|
||||
title = "Rafraîchir Twitch",
|
||||
icon = "refresh",
|
||||
muted = true,
|
||||
value = { "script-message", "feeds-open", "twitch", "refresh" },
|
||||
},
|
||||
},
|
||||
})
|
||||
end)
|
||||
13
dotfiles/mpv/twitch.csv
Normal file
13
dotfiles/mpv/twitch.csv
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Wissam_Xelka
|
||||
HasanAbi
|
||||
Zawa_Prod
|
||||
Cass_Andre
|
||||
Ostpolitik
|
||||
dofla
|
||||
LeMawakast
|
||||
ParolesDHonneur_
|
||||
lives2leo
|
||||
MOuffette_
|
||||
KaLeeVision
|
||||
Bamwempan
|
||||
RiboDansLaSauce
|
||||
|
101
dotfiles/mpv/youtube.csv
Normal file
101
dotfiles/mpv/youtube.csv
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
ID des chaînes,URL des chaînes,Titres des chaînes
|
||||
UC0HxyEc_ojRJ1oJXS5K6oaA,http://www.youtube.com/channel/UC0HxyEc_ojRJ1oJXS5K6oaA,Politikon
|
||||
UC0Miwa4YsGduqJFYmpCSXeg,http://www.youtube.com/channel/UC0Miwa4YsGduqJFYmpCSXeg,taoqan
|
||||
UC0fDG3byEcMtbOqPMymDNbw,http://www.youtube.com/channel/UC0fDG3byEcMtbOqPMymDNbw,/noclip
|
||||
UC0yPCUmdMZIGtnxSnx5_ifA,http://www.youtube.com/channel/UC0yPCUmdMZIGtnxSnx5_ifA,Tzitzimitl - Esprit Critique
|
||||
UC1ObD9AuLfxiqRkrY7A_kgA,http://www.youtube.com/channel/UC1ObD9AuLfxiqRkrY7A_kgA,Les vidéos de Léo
|
||||
UC1eBAh5DIkn9N_5XzMS0wFg,http://www.youtube.com/channel/UC1eBAh5DIkn9N_5XzMS0wFg,Les Amoureuses
|
||||
UC1zVvHsRsvxInUMX4dfIyAQ,http://www.youtube.com/channel/UC1zVvHsRsvxInUMX4dfIyAQ,The Sciencoder
|
||||
UC2qlgiYCCtaYpn2_blX01xg,http://www.youtube.com/channel/UC2qlgiYCCtaYpn2_blX01xg,Antoine Goya
|
||||
UC2rpRj3VqEMaT7vgtiBnQoQ,http://www.youtube.com/channel/UC2rpRj3VqEMaT7vgtiBnQoQ,Veridis Project
|
||||
UC3A_TG1leX0eQEJD1Ew6Ftw,http://www.youtube.com/channel/UC3A_TG1leX0eQEJD1Ew6Ftw,Game Of Hearth
|
||||
UC476clcLNhTKwjdulsfvHLw,http://www.youtube.com/channel/UC476clcLNhTKwjdulsfvHLw,imranepresents
|
||||
UC4vfXTvb9fysmiGab_cdJXg,http://www.youtube.com/channel/UC4vfXTvb9fysmiGab_cdJXg,Legendes Musique
|
||||
UC5AA57FT52rBCyGc9UT1DDg,http://www.youtube.com/channel/UC5AA57FT52rBCyGc9UT1DDg,Emmodem VOD
|
||||
UC5eOLQO5VUEFJukNg9cl5jg,http://www.youtube.com/channel/UC5eOLQO5VUEFJukNg9cl5jg,Dave Sheik
|
||||
UC6zC37ITu2dNx3GimE8UV2A,http://www.youtube.com/channel/UC6zC37ITu2dNx3GimE8UV2A,Mathys Aurand
|
||||
UC7sXGI8p8PvKosLWagkK9wQ,http://www.youtube.com/channel/UC7sXGI8p8PvKosLWagkK9wQ,Heu?reka
|
||||
UC85YiwWx6BC6SYR7wuMyG8A,http://www.youtube.com/channel/UC85YiwWx6BC6SYR7wuMyG8A,SLICE Histoire
|
||||
UC8cQpBzgnpkHPMkEpnoWWcQ,http://www.youtube.com/channel/UC8cQpBzgnpkHPMkEpnoWWcQ,Three Hours Later
|
||||
UC9_qAALb4xSC4cWyuzECs5g,http://www.youtube.com/channel/UC9_qAALb4xSC4cWyuzECs5g,Saturne
|
||||
UC9lWSxp7TdohmRwnvSR2Gvw,http://www.youtube.com/channel/UC9lWSxp7TdohmRwnvSR2Gvw,RiboDansLaSauce
|
||||
UC9mes7ZXbrZ_UXn0jzBt21A,http://www.youtube.com/channel/UC9mes7ZXbrZ_UXn0jzBt21A,Bon Pote
|
||||
UC9r6ydGn4TA8PexLQlgWSmQ,http://www.youtube.com/channel/UC9r6ydGn4TA8PexLQlgWSmQ,Réflexions Basses
|
||||
UC9w4KQLmoOOBhhFwcKnK7UA,http://www.youtube.com/channel/UC9w4KQLmoOOBhhFwcKnK7UA,Max Atger
|
||||
UCAJta1-T5V_RB7YDfp7GQag,http://www.youtube.com/channel/UCAJta1-T5V_RB7YDfp7GQag,Headlundas
|
||||
UCAbFIrKZCYdKucQYnhxWnrA,http://www.youtube.com/channel/UCAbFIrKZCYdKucQYnhxWnrA,LIMIT
|
||||
UCBm4zCKYn0Iirik1j5yRTfg,http://www.youtube.com/channel/UCBm4zCKYn0Iirik1j5yRTfg,Satellisés
|
||||
UCCTbruTofQHTMkmG8cEJ4fQ,http://www.youtube.com/channel/UCCTbruTofQHTMkmG8cEJ4fQ,Jack Couscous
|
||||
UCCTuFofVWD9uORhQqgn-d7Q,http://www.youtube.com/channel/UCCTuFofVWD9uORhQqgn-d7Q,INA Société
|
||||
UCCauEjHzPXxGer8af3u4x5g,http://www.youtube.com/channel/UCCauEjHzPXxGer8af3u4x5g,Envoyé Spécial
|
||||
UCDanmCiN4kaOO_tOOv81s6g,http://www.youtube.com/channel/UCDanmCiN4kaOO_tOOv81s6g,Emmodem Vod
|
||||
UCEFGCs68E9Vr5te5qZuJsbg,http://www.youtube.com/channel/UCEFGCs68E9Vr5te5qZuJsbg,Off Investigation
|
||||
UCEfpbmshpf-G90fGLla73kA,http://www.youtube.com/channel/UCEfpbmshpf-G90fGLla73kA,Bolchegeek
|
||||
UCEsShazY9frY_x51rknY2dw,http://www.youtube.com/channel/UCEsShazY9frY_x51rknY2dw,Dandee
|
||||
UCF8wHRQmuBpOuvQvsfSFMnQ,http://www.youtube.com/channel/UCF8wHRQmuBpOuvQvsfSFMnQ,Faustin Sullivan
|
||||
UCGjKwjEvPA0ms2wOyBwdMPQ,http://www.youtube.com/channel/UCGjKwjEvPA0ms2wOyBwdMPQ,Parlons Cyber
|
||||
UCHOtlkL8XEHbL6KHIHYRGSw,http://www.youtube.com/channel/UCHOtlkL8XEHbL6KHIHYRGSw,Franck Lepage
|
||||
UCHiwtz2tCEfS17N9A-WoSSw,http://www.youtube.com/channel/UCHiwtz2tCEfS17N9A-WoSSw,Pop Culture Detective
|
||||
UCI_FnSJPNGd3Y3IL0PwD-NQ,http://www.youtube.com/channel/UCI_FnSJPNGd3Y3IL0PwD-NQ,Diable Positif
|
||||
UCIlNSCHPwun7060OYU5thrw,http://www.youtube.com/channel/UCIlNSCHPwun7060OYU5thrw,Barap
|
||||
UCJ9kIKgFuAB23GkEko1hHDg,http://www.youtube.com/channel/UCJ9kIKgFuAB23GkEko1hHDg,Mycéliums
|
||||
UCJT6bmaCnlJDq5Yh1EfoSQw,http://www.youtube.com/channel/UCJT6bmaCnlJDq5Yh1EfoSQw,Grünt
|
||||
UCJd1AFjN9vIgVDDB6UE2UQg,http://www.youtube.com/channel/UCJd1AFjN9vIgVDDB6UE2UQg,Marouchka de Qu'est-ce qu'on lit ?
|
||||
UCJpIF-QIOk1YOjSqD5eqyqQ,http://www.youtube.com/channel/UCJpIF-QIOk1YOjSqD5eqyqQ,Elyne Simon
|
||||
UCLXDNUOO3EQ80VmD9nQBHPg,http://www.youtube.com/channel/UCLXDNUOO3EQ80VmD9nQBHPg,Fouloscopie
|
||||
UCLdmnkqdcTPHvVZ8aNdbf5A,http://www.youtube.com/channel/UCLdmnkqdcTPHvVZ8aNdbf5A,Spline LND
|
||||
UCNxe55xhsqp_w6k6GEor8Uw,http://www.youtube.com/channel/UCNxe55xhsqp_w6k6GEor8Uw,Papy fait de la Resistance
|
||||
UCP46_MXP_WG_auH88FnfS1A,http://www.youtube.com/channel/UCP46_MXP_WG_auH88FnfS1A,Nota Bene
|
||||
UCPTlw9-dflN3_Sw9AfQs3vw,http://www.youtube.com/channel/UCPTlw9-dflN3_Sw9AfQs3vw,Faune Cool
|
||||
UCPVS8efBdhXGGYs4JW2IJcQ,http://www.youtube.com/channel/UCPVS8efBdhXGGYs4JW2IJcQ,Kiffe ta race
|
||||
UCPZIJMFJBoyG7t7nsUQ3UcA,http://www.youtube.com/channel/UCPZIJMFJBoyG7t7nsUQ3UcA,Nedrac
|
||||
UCPo1nFSGNkyzrA9yvtkEvIw,http://www.youtube.com/channel/UCPo1nFSGNkyzrA9yvtkEvIw,Eliott Meunier
|
||||
UCQHX6ViZmPsWiYSFAyS0a3Q,http://www.youtube.com/channel/UCQHX6ViZmPsWiYSFAyS0a3Q,GothamChess
|
||||
UCQcas4dulTzVUrAo0fMfWWg,http://www.youtube.com/channel/UCQcas4dulTzVUrAo0fMfWWg,Clément Viktorovitch
|
||||
UCRoQyGLLPd9lwPUJtUwD2yw,http://www.youtube.com/channel/UCRoQyGLLPd9lwPUJtUwD2yw,Samora | Le Mwakast
|
||||
UCT67YOMntJxfRnO_9bXDpvw,http://www.youtube.com/channel/UCT67YOMntJxfRnO_9bXDpvw,Le Média
|
||||
UCTth6-5ZiCD_CUJ6xOMug_w,http://www.youtube.com/channel/UCTth6-5ZiCD_CUJ6xOMug_w,Paroles D'Honneur
|
||||
UCUc-m8A95mE9zLlNvVtLfFQ,http://www.youtube.com/channel/UCUc-m8A95mE9zLlNvVtLfFQ,Orange Trio Music
|
||||
UCVeMw72tepFl1Zt5fvf9QKQ,http://www.youtube.com/channel/UCVeMw72tepFl1Zt5fvf9QKQ,Osons Causer
|
||||
UCVuMMUfqEI448VKbkpuNPHQ,http://www.youtube.com/channel/UCVuMMUfqEI448VKbkpuNPHQ,Histoires Crépues
|
||||
UCWGsN59FON3vOtXCozQPsZQ,http://www.youtube.com/channel/UCWGsN59FON3vOtXCozQPsZQ,Canard Réfractaire
|
||||
UCXHWT_QoQSsdGXbqF0hAMtg,http://www.youtube.com/channel/UCXHWT_QoQSsdGXbqF0hAMtg,Arrêt sur images
|
||||
UCYpRDnhk5H8h16jpS84uqsA,http://www.youtube.com/channel/UCYpRDnhk5H8h16jpS84uqsA,Le Monde
|
||||
UC_-hYjoNe4PJNFa9iZ4lraA,http://www.youtube.com/channel/UC_-hYjoNe4PJNFa9iZ4lraA,Good Work
|
||||
UC_2-ii_May8L-lx25YJhSew,http://www.youtube.com/channel/UC_2-ii_May8L-lx25YJhSew,Fracas
|
||||
UC_NukSq2ve_AHVAz_9ikTDg,http://www.youtube.com/channel/UC_NukSq2ve_AHVAz_9ikTDg,Usul
|
||||
UC__xRB5L4toU9yYawt_lIKg,http://www.youtube.com/channel/UC__xRB5L4toU9yYawt_lIKg,"BLAST, Le souffle de l'info"
|
||||
UCaU9xFZi08_meRCb9Uzr_Ew,http://www.youtube.com/channel/UCaU9xFZi08_meRCb9Uzr_Ew,L'Argumentarium
|
||||
UCafxR2HWJRmMfSdyZXvZMTw,http://www.youtube.com/channel/UCafxR2HWJRmMfSdyZXvZMTw,LOOK MUM NO COMPUTER
|
||||
UCar0yo-PpeMk51m116826pQ,http://www.youtube.com/channel/UCar0yo-PpeMk51m116826pQ,Journal l'Humanité
|
||||
UCd5DKToXYTKAQ6khzewww2g,http://www.youtube.com/channel/UCd5DKToXYTKAQ6khzewww2g,France Culture
|
||||
UCdKTlsmvczkdvGjiLinQwmw,http://www.youtube.com/channel/UCdKTlsmvczkdvGjiLinQwmw,Philoxime
|
||||
UCdnaDhU-LDQrIEEmSIfq0-Q,http://www.youtube.com/channel/UCdnaDhU-LDQrIEEmSIfq0-Q,Mediapart
|
||||
UCe6iUNw4s5VxGnWi63JRU3g,http://www.youtube.com/channel/UCe6iUNw4s5VxGnWi63JRU3g,Bamwempan
|
||||
UCeQRERCvEY3-ZeQONwnx0zw,http://www.youtube.com/channel/UCeQRERCvEY3-ZeQONwnx0zw,Zawa Prod
|
||||
UCebfFtcRXJWGgAHi0h8n2JA,http://www.youtube.com/channel/UCebfFtcRXJWGgAHi0h8n2JA,Sismique
|
||||
UCeovElJP0n0i8ADaPsRSd8g,http://www.youtube.com/channel/UCeovElJP0n0i8ADaPsRSd8g,HAINBACH
|
||||
UCg5OUM5Y5UWJHJgFCjXcfkA,http://www.youtube.com/channel/UCg5OUM5Y5UWJHJgFCjXcfkA,Way Back 2 Afrika
|
||||
UChG8nxeVTk6jQJyH-_TG7xw,http://www.youtube.com/channel/UChG8nxeVTk6jQJyH-_TG7xw,InPower Podcast
|
||||
UChYdEwKt1QyP27OwzmVObnA,http://www.youtube.com/channel/UChYdEwKt1QyP27OwzmVObnA,Blast2
|
||||
UChaI6XKLJJHZu70_QJFQjKA,http://www.youtube.com/channel/UChaI6XKLJJHZu70_QJFQjKA,Gael Berlinger
|
||||
UCi1h68Fys0apnhLaJ-ijQOw,http://www.youtube.com/channel/UCi1h68Fys0apnhLaJ-ijQOw,INA Politique
|
||||
UCkgO4A3Fzm5D9Xu1Y_4vCKQ,http://www.youtube.com/channel/UCkgO4A3Fzm5D9Xu1Y_4vCKQ,ÉLUCID
|
||||
UCn1eIYIqfPOhj-KhDmbG8KA,http://www.youtube.com/channel/UCn1eIYIqfPOhj-KhDmbG8KA,AMI | INSA Lyon
|
||||
UCnP7HeJGVy8EL1L-1gTDm8w,http://www.youtube.com/channel/UCnP7HeJGVy8EL1L-1gTDm8w,STUPMEDIA
|
||||
UCoobqoKhnrJhgP8v2bx3ZnA,http://www.youtube.com/channel/UCoobqoKhnrJhgP8v2bx3ZnA,SOCIOLOGIK
|
||||
UCp9cUr3zrGguWsm5oIa2VAQ,http://www.youtube.com/channel/UCp9cUr3zrGguWsm5oIa2VAQ,Bignou Collectif
|
||||
UCqt99sKYNTxqlHtzV9weUYA,http://www.youtube.com/channel/UCqt99sKYNTxqlHtzV9weUYA,VU FranceTV
|
||||
UCrzwP_TnWL_FA-PjIZ9qaZg,http://www.youtube.com/channel/UCrzwP_TnWL_FA-PjIZ9qaZg,Cass Andre
|
||||
UCt3XYjlua5RtcHyEKUkUKOQ,http://www.youtube.com/channel/UCt3XYjlua5RtcHyEKUkUKOQ,Kong DIRTY BOXING
|
||||
UCvW-hY30MCw1qrxAN2HeeZg,http://www.youtube.com/channel/UCvW-hY30MCw1qrxAN2HeeZg,À L'Assaut du Ciel
|
||||
UCwI-JbGNsojunnHbFAc0M4Q,http://www.youtube.com/channel/UCwI-JbGNsojunnHbFAc0M4Q,ARTE
|
||||
UCwU7Gq3vG5kL1eknXIZvMag,http://www.youtube.com/channel/UCwU7Gq3vG5kL1eknXIZvMag,Wissam Xelka
|
||||
UCwaliuWsJWhdhk-KUfQHlYA,http://www.youtube.com/channel/UCwaliuWsJWhdhk-KUfQHlYA,Metabolism of Cities
|
||||
UCweCc7bSMX5J4jEH7HFImng,http://www.youtube.com/channel/UCweCc7bSMX5J4jEH7HFImng,GMHikaru
|
||||
UCxJka_qnTVquoIL-PQV5POg,http://www.youtube.com/channel/UCxJka_qnTVquoIL-PQV5POg,DanyCaligula
|
||||
UCy4CtZFhDCSkNnr6GUypacw,http://www.youtube.com/channel/UCy4CtZFhDCSkNnr6GUypacw,Síomha
|
||||
UCyJDHgrsUKuWLe05GvC2lng,http://www.youtube.com/channel/UCyJDHgrsUKuWLe05GvC2lng,Stupid Economics
|
||||
UCyjXo7RjhDQ1oX7Q7CPUUIQ,http://www.youtube.com/channel/UCyjXo7RjhDQ1oX7Q7CPUUIQ,Avant l'orage
|
||||
UCzB5vMdtF0qAf84nV3HrD2A,http://www.youtube.com/channel/UCzB5vMdtF0qAf84nV3HrD2A,Thomas Babord
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue