├── LICENSE ├── watchlist_film.js ├── watchlist_serial.js ├── watched_serial.js ├── watched_film.js └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jakub Serwatka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /watchlist_film.js: -------------------------------------------------------------------------------- 1 | function formatDate(dateNumber) { 2 | const dateStr = dateNumber?.toString(); 3 | if (!dateStr) { 4 | return `2000-01-01` 5 | } 6 | 7 | const year = dateStr.substring(0, 4); 8 | const month = dateStr.substring(4, 6); 9 | const day = dateStr.substring(6, 8); 10 | 11 | return `${year}-${month}-${day}`; 12 | } 13 | 14 | async function fetchApi(endpoint) { 15 | const dataJSON = await fetch(`https://www.filmweb.pl/api/v1/${endpoint}`, { 16 | method: "GET", 17 | headers: { 18 | Cookie: document.cookie, 19 | 'X-Locale': 'pl', 20 | } 21 | }) 22 | .then((response) => { 23 | if (!response.ok) { 24 | throw Error(`Błąd skryptu podczas fetchowania, endpoint: ${endpoint}, reponse: ${JSON.stringify(response)}`) 25 | } 26 | return response.json() 27 | }) 28 | 29 | return dataJSON; 30 | } 31 | 32 | async function getAllRates(resourceType) { 33 | const allData = []; 34 | const dataJSON = await fetchApi(`logged/want2see?entityName=${resourceType}`) 35 | if (resourceType === "serial") { 36 | const tvseriesJSON = await fetchApi(`logged/want2see?entityName=tvshow`) 37 | dataJSON.push(...tvseriesJSON) 38 | } 39 | const allSavedIds = dataJSON.filter((entry) => entry[1] > 0).map((entry) => entry[0]) 40 | 41 | for (let i = 0; i < allSavedIds.length; i++) { 42 | // get title, year 43 | const restOfData = await fetchApi(`title/${allSavedIds[i]}/info`); 44 | const title = restOfData["originalTitle"] ? restOfData["originalTitle"] : restOfData["title"] 45 | allData.push({ 46 | Title: title.includes(",") ? `"${title}"` : title, 47 | Year: restOfData.year, 48 | }) 49 | 50 | console.log("pobrano " + (i + 1)) 51 | } 52 | 53 | return allData; 54 | } 55 | 56 | function download(filename, text) { 57 | const element = document.createElement("a"); 58 | element.setAttribute( 59 | "href", 60 | "data:text/plain;charset=utf-8," + encodeURIComponent(text) 61 | ); 62 | element.setAttribute("download", filename); 63 | element.style.display = "none"; 64 | document.body.appendChild(element); 65 | 66 | element.click(); 67 | 68 | document.body.removeChild(element); 69 | } 70 | 71 | function arrayToCsv(allRates) { 72 | let csvRates = Object.keys(allRates[0]).join(",") + "\n"; 73 | let filesArray = []; 74 | 75 | for (let i = 0; i < allRates.length; i++) { 76 | // Split csv for rate count > 1800 77 | if (i % 1799 == 0 && i > 0) { 78 | filesArray.push(csvRates); 79 | csvRates = Object.keys(allRates[0]).join(",") + "\n"; 80 | } 81 | csvRates += Object.values(allRates[i]).join(","); 82 | csvRates += "\n"; 83 | } 84 | filesArray.push(csvRates); 85 | 86 | return filesArray; 87 | } 88 | 89 | async function main(resourceType) { 90 | console.log("Rozpoczynam pobieranie, cierpliwości..."); 91 | console.log("Proszę nie zamykać, przełączać, ani minimalizować tego okna!"); 92 | let allRates = await getAllRates(resourceType); 93 | console.log("rozpoczynam ściąganie pliku csv"); 94 | let csvRates = arrayToCsv(allRates); 95 | 96 | for (let i = 0; i < csvRates.length; i++) { 97 | download(`Filmweb2Letterboxd_watchlist_${resourceType}_${i}.csv`, csvRates[i]); 98 | } 99 | } 100 | 101 | main("film"); -------------------------------------------------------------------------------- /watchlist_serial.js: -------------------------------------------------------------------------------- 1 | function formatDate(dateNumber) { 2 | const dateStr = dateNumber?.toString(); 3 | if (!dateStr) { 4 | return `2000-01-01` 5 | } 6 | 7 | const year = dateStr.substring(0, 4); 8 | const month = dateStr.substring(4, 6); 9 | const day = dateStr.substring(6, 8); 10 | 11 | return `${year}-${month}-${day}`; 12 | } 13 | 14 | async function fetchApi(endpoint) { 15 | const dataJSON = await fetch(`https://www.filmweb.pl/api/v1/${endpoint}`, { 16 | method: "GET", 17 | headers: { 18 | Cookie: document.cookie, 19 | 'X-Locale': 'pl', 20 | } 21 | }) 22 | .then((response) => { 23 | if (!response.ok) { 24 | throw Error(`Błąd skryptu podczas fetchowania, endpoint: ${endpoint}, reponse: ${JSON.stringify(response)}`) 25 | } 26 | return response.json() 27 | }) 28 | 29 | return dataJSON; 30 | } 31 | 32 | async function getAllRates(resourceType) { 33 | const allData = []; 34 | const dataJSON = await fetchApi(`logged/want2see?entityName=${resourceType}`) 35 | if (resourceType === "serial") { 36 | const tvseriesJSON = await fetchApi(`logged/want2see?entityName=tvshow`) 37 | dataJSON.push(...tvseriesJSON) 38 | } 39 | const allSavedIds = dataJSON.filter((entry) => entry[1] > 0).map((entry) => entry[0]) 40 | 41 | for (let i = 0; i < allSavedIds.length; i++) { 42 | // get title, year 43 | const restOfData = await fetchApi(`title/${allSavedIds[i]}/info`); 44 | const title = restOfData["originalTitle"] ? restOfData["originalTitle"] : restOfData["title"] 45 | allData.push({ 46 | Title: title.includes(",") ? `"${title}"` : title, 47 | Year: restOfData.year, 48 | }) 49 | 50 | console.log("pobrano " + (i + 1)) 51 | } 52 | 53 | return allData; 54 | } 55 | 56 | function download(filename, text) { 57 | const element = document.createElement("a"); 58 | element.setAttribute( 59 | "href", 60 | "data:text/plain;charset=utf-8," + encodeURIComponent(text) 61 | ); 62 | element.setAttribute("download", filename); 63 | element.style.display = "none"; 64 | document.body.appendChild(element); 65 | 66 | element.click(); 67 | 68 | document.body.removeChild(element); 69 | } 70 | 71 | function arrayToCsv(allRates) { 72 | let csvRates = Object.keys(allRates[0]).join(",") + "\n"; 73 | let filesArray = []; 74 | 75 | for (let i = 0; i < allRates.length; i++) { 76 | // Split csv for rate count > 1800 77 | if (i % 1799 == 0 && i > 0) { 78 | filesArray.push(csvRates); 79 | csvRates = Object.keys(allRates[0]).join(",") + "\n"; 80 | } 81 | csvRates += Object.values(allRates[i]).join(","); 82 | csvRates += "\n"; 83 | } 84 | filesArray.push(csvRates); 85 | 86 | return filesArray; 87 | } 88 | 89 | async function main(resourceType) { 90 | console.log("Rozpoczynam pobieranie, cierpliwości..."); 91 | console.log("Proszę nie zamykać, przełączać, ani minimalizować tego okna!"); 92 | let allRates = await getAllRates(resourceType); 93 | console.log("rozpoczynam ściąganie pliku csv"); 94 | let csvRates = arrayToCsv(allRates); 95 | 96 | for (let i = 0; i < csvRates.length; i++) { 97 | download(`Filmweb2Letterboxd_watchlist_${resourceType}_${i}.csv`, csvRates[i]); 98 | } 99 | } 100 | 101 | main("serial"); -------------------------------------------------------------------------------- /watched_serial.js: -------------------------------------------------------------------------------- 1 | function formatDate(dateNumber) { 2 | const dateStr = dateNumber?.toString(); 3 | if (!dateStr) { 4 | return `2000-01-01` 5 | } 6 | 7 | const year = dateStr.substring(0, 4); 8 | const month = dateStr.substring(4, 6); 9 | const day = dateStr.substring(6, 8); 10 | 11 | return `${year}-${month}-${day}`; 12 | } 13 | 14 | async function fetchApi(endpoint) { 15 | const dataJSON = await fetch(`https://www.filmweb.pl/api/v1/${endpoint}`, { 16 | method: "GET", 17 | headers: { 18 | Cookie: document.cookie, 19 | 'X-Locale': 'pl', 20 | } 21 | }) 22 | .then((response) => { 23 | if (!response.ok) { 24 | throw Error(`Błąd skryptu podczas fetchowania, endpoint: ${endpoint}, reponse: ${JSON.stringify(response)}`) 25 | } 26 | return response.json() 27 | }) 28 | 29 | return dataJSON; 30 | } 31 | 32 | async function getAllRates(resourceType) { 33 | let nextPage = 1 34 | const allVotes = [] 35 | const allData = []; 36 | 37 | while (true) { 38 | // get rating, id, viewDate 39 | const dataJSON = await fetchApi(`logged/vote/title/${resourceType}?page=${nextPage}`) 40 | if (dataJSON.length == 0) { 41 | break 42 | } 43 | 44 | allVotes.push(...dataJSON) 45 | nextPage += 1; 46 | } 47 | 48 | if (resourceType === "serial") { 49 | nextPage = 1 50 | 51 | while (true) { 52 | const tvseriesJSON = await fetchApi(`logged/vote/title/tvshow?page=${nextPage}`) 53 | if (tvseriesJSON.length == 0) { 54 | break 55 | } 56 | 57 | allVotes.push(...tvseriesJSON) 58 | nextPage += 1; 59 | } 60 | } 61 | 62 | for (let i = 0; i < allVotes.length; i++) { 63 | const vote = allVotes[i] 64 | 65 | const id = vote["entity"]; 66 | if (!id) { 67 | throw Error(`Film nie znaleziony: ${JSON.stringify(vote)}`) 68 | } 69 | // get title, year 70 | const restOfData = await fetchApi(`title/${id}/info`); 71 | const title = restOfData["originalTitle"] ? restOfData["originalTitle"] : restOfData["title"] 72 | allData.push({ 73 | Title: title.includes(",") ? `"${title}"` : title, 74 | Year: restOfData.year, 75 | WatchedDate: formatDate(vote.viewDate), 76 | Rating10: vote.rate > 0 ? vote.rate : null 77 | }) 78 | 79 | console.log("pobrano " + (i + 1)) 80 | } 81 | 82 | return allData; 83 | } 84 | 85 | function download(filename, text) { 86 | const element = document.createElement("a"); 87 | element.setAttribute( 88 | "href", 89 | "data:text/plain;charset=utf-8," + encodeURIComponent(text) 90 | ); 91 | element.setAttribute("download", filename); 92 | element.style.display = "none"; 93 | document.body.appendChild(element); 94 | 95 | element.click(); 96 | 97 | document.body.removeChild(element); 98 | } 99 | 100 | function arrayToCsv(allRates) { 101 | let csvRates = Object.keys(allRates[0]).join(",") + "\n"; 102 | let filesArray = []; 103 | 104 | for (let i = 0; i < allRates.length; i++) { 105 | // Split csv for rate count > 1800 106 | if (i % 1799 == 0 && i > 0) { 107 | filesArray.push(csvRates); 108 | csvRates = Object.keys(allRates[0]).join(",") + "\n"; 109 | } 110 | csvRates += Object.values(allRates[i]).join(","); 111 | csvRates += "\n"; 112 | } 113 | filesArray.push(csvRates); 114 | 115 | return filesArray; 116 | } 117 | 118 | async function main(resourceType) { 119 | console.log("Rozpoczynam pobieranie, cierpliwości..."); 120 | console.log("Proszę nie zamykać, przełączać, ani minimalizować tego okna!"); 121 | let allRates = await getAllRates(resourceType); 122 | console.log("rozpoczynam ściąganie pliku csv"); 123 | let csvRates = arrayToCsv(allRates); 124 | 125 | for (let i = 0; i < csvRates.length; i++) { 126 | download(`Filmweb2Letterboxd_watched_${resourceType}_${i}.csv`, csvRates[i]); 127 | } 128 | } 129 | 130 | main("serial"); -------------------------------------------------------------------------------- /watched_film.js: -------------------------------------------------------------------------------- 1 | function formatDate(dateNumber) { 2 | const dateStr = dateNumber?.toString(); 3 | if (!dateStr) { 4 | return `2000-01-01` 5 | } 6 | 7 | const year = dateStr.substring(0, 4); 8 | const month = dateStr.substring(4, 6); 9 | const day = dateStr.substring(6, 8); 10 | 11 | return `${year}-${month}-${day}`; 12 | } 13 | 14 | async function fetchApi(endpoint) { 15 | const dataJSON = await fetch(`https://www.filmweb.pl/api/v1/${endpoint}`, { 16 | method: "GET", 17 | headers: { 18 | Cookie: document.cookie, 19 | 'X-Locale': 'pl', 20 | } 21 | }) 22 | .then((response) => { 23 | if (!response.ok) { 24 | throw Error(`Błąd skryptu podczas fetchowania, endpoint: ${endpoint}, reponse: ${JSON.stringify(response)}`) 25 | } 26 | return response.json() 27 | }) 28 | 29 | return dataJSON; 30 | } 31 | 32 | async function getAllRates(resourceType) { 33 | let nextPage = 1 34 | const allVotes = [] 35 | const allData = []; 36 | 37 | while (true) { 38 | // get rating, id, viewDate 39 | const dataJSON = await fetchApi(`logged/vote/title/${resourceType}?page=${nextPage}`) 40 | if (dataJSON.length == 0) { 41 | break 42 | } 43 | 44 | allVotes.push(...dataJSON) 45 | nextPage += 1; 46 | } 47 | 48 | if (resourceType === "serial") { 49 | nextPage = 1 50 | 51 | while (true) { 52 | const tvseriesJSON = await fetchApi(`logged/vote/title/tvshow?page=${nextPage}`) 53 | if (tvseriesJSON.length == 0) { 54 | break 55 | } 56 | 57 | allVotes.push(...tvseriesJSON) 58 | nextPage += 1; 59 | } 60 | } 61 | 62 | for (let i = 0; i < allVotes.length; i++) { 63 | const vote = allVotes[i] 64 | 65 | const id = vote["entity"]; 66 | if (!id) { 67 | throw Error(`Film nie znaleziony: ${JSON.stringify(vote)}`) 68 | } 69 | // get title, year 70 | const restOfData = await fetchApi(`title/${id}/info`); 71 | const title = restOfData["originalTitle"] ? restOfData["originalTitle"] : restOfData["title"] 72 | allData.push({ 73 | Title: title.includes(",") ? `"${title}"` : title, 74 | Year: restOfData.year, 75 | WatchedDate: formatDate(vote.viewDate), 76 | Rating10: vote.rate > 0 ? vote.rate : null 77 | }) 78 | 79 | console.log("pobrano " + (i + 1)) 80 | } 81 | 82 | return allData; 83 | } 84 | 85 | function download(filename, text) { 86 | const element = document.createElement("a"); 87 | element.setAttribute( 88 | "href", 89 | "data:text/plain;charset=utf-8," + encodeURIComponent(text) 90 | ); 91 | element.setAttribute("download", filename); 92 | element.style.display = "none"; 93 | document.body.appendChild(element); 94 | 95 | element.click(); 96 | 97 | document.body.removeChild(element); 98 | } 99 | 100 | function arrayToCsv(allRates) { 101 | let csvRates = Object.keys(allRates[0]).join(",") + "\n"; 102 | let filesArray = []; 103 | 104 | for (let i = 0; i < allRates.length; i++) { 105 | // Split csv for rate count > 1800 106 | if (i % 1799 == 0 && i > 0) { 107 | filesArray.push(csvRates); 108 | csvRates = Object.keys(allRates[0]).join(",") + "\n"; 109 | } 110 | csvRates += Object.values(allRates[i]).join(","); 111 | csvRates += "\n"; 112 | } 113 | filesArray.push(csvRates); 114 | 115 | return filesArray; 116 | } 117 | 118 | async function main(resourceType) { 119 | console.log("Rozpoczynam pobieranie, cierpliwości..."); 120 | console.log("Proszę nie zamykać, przełączać, ani minimalizować tego okna!"); 121 | let allRates = await getAllRates(resourceType); 122 | console.log("rozpoczynam ściąganie pliku csv"); 123 | let csvRates = arrayToCsv(allRates); 124 | 125 | for (let i = 0; i < csvRates.length; i++) { 126 | download(`Filmweb2Letterboxd_watched_${resourceType}_${i}.csv`, csvRates[i]); 127 | } 128 | } 129 | 130 | main("film"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filmweb2Letterboxd 2 | Postaw mi kawę na buycoffee.to 3 | 4 | Sprawdź też [Letterboxd Tweaks](https://chromewebstore.google.com/detail/letterboxd-tweaks/hopfbphfhmjgdnedoldfpbhepohibfkj) 5 | ## O projekcie 6 | 7 | Skrypty eksportują oceny filmów/seriali i "Chcę zobaczyć" z Filmweb'u do formatu csv. 8 | Pliku csv ma następujące kolumny: 9 | 10 | - _Title_ - oryginalny tytuł programu, a jak nie znajdzie, to polski; 11 | - _Year_ - rok produkcji programu; 12 | - _Rating10_ - ocena użytkownika; 13 | - _WatchedDate_ - data obejrzenia. 14 | 15 | ## Jak używać 16 | 17 | ### Pobranie obejrzanych filmów lub seriali 18 | 19 | 1. Zaloguj się do Filmweb'a. 20 | 2. Przejdź do `https://www.filmweb.pl/user/`. 21 | 3. Otwórz konsolę 22 | ![image](https://github.com/JSerwatka/Filmweb2Letterboxd/assets/33938646/2b4ce2e1-0556-4fbd-b29c-1cb957918ca4) 23 | 24 | - Windows: `Ctrl + Shift + J` 25 | - Mac: `Cmd + Option + J` 26 | 5. Wklej skrypt z pliku 27 | - Dla filmów: [watched_film.js](https://github.com/JSerwatka/Filmweb2Letterboxd/blob/master/watched_film.js) 28 | - Dla seriali: [watched_serial.js](https://github.com/JSerwatka/Filmweb2Letterboxd/blob/master/watched_serial.js) 29 | 6. Zostaw kartę otwartą i nie przełączaj na żadną inną! 30 | 31 | Dalej masz wątpliwości, sprawdź ten komentarz: https://github.com/JSerwatka/Filmweb2Letterboxd/issues/8#issuecomment-1887372746. 32 | 33 | ### Pobranie "Chcę zobaczyć" 34 | 35 | 1. Zaloguj się do Filmweb'a. 36 | 2. Przejdź do `https://www.filmweb.pl/user/`. 37 | 3. Otwórz konsolę 38 | ![image](https://github.com/JSerwatka/Filmweb2Letterboxd/assets/33938646/ced6335c-e391-4715-85ee-1ed95bfa0d9e) 39 | 40 | - Windows: `Ctrl + Shift + J` 41 | - Mac: `Cmd + Option + J` 42 | 5. Wklej skrypt z pliku 43 | - Dla filmów: [watchlist_film.js](https://github.com/JSerwatka/Filmweb2Letterboxd/blob/master/watchlist_film.js) 44 | - Dla seriali: [watchlist_serial.js](https://github.com/JSerwatka/Filmweb2Letterboxd/blob/master/watchlist_serial.js) 45 | 6. Zostaw kartę otwartą i nie przełączaj na żadną inną! 46 | 47 | ## Jak zaimportować dane do Letterboxd 48 | Zaloguj się, a następnie: 49 | - Obejrzane filmy: 50 | - Wejdź na _https://letterboxd.com/import/_ -> _Select a file_ 51 | ![image](https://github.com/JSerwatka/Filmweb2Letterboxd/assets/33938646/12695a0f-2340-4b51-b0fc-0eb230c6dfc5) 52 | 53 | - Wczytaj wszystkie pliki, którę zostały ściągnięte 54 | - Po zakończeniu importowania przełącz _Hide successful matches_. Pojawią Ci się wtedy wszystkie filmy, których nie udało się zaimportować. Spróbuj wyszukać i dodać je ręcznie. 55 | - Obejrzene seriale: Letterboxd obecnie ma niewielkie wsparcie dla seriali (będzie one wprowadzane w 2024 roku), ale możesz spróbować zrobić to samo, co w przypadku filmów. 56 | - "Chcę zobaczyć" filmy: 57 | - Wejdź na _https://letterboxd.com/import/_ -> _import to you watchlist_ -> _Import films to watchlist..._ 58 | ![image](https://github.com/JSerwatka/Filmweb2Letterboxd/assets/33938646/2f9c02b0-efe1-4bc6-bfeb-409fd7adb5b5) 59 | ![image](https://github.com/JSerwatka/Filmweb2Letterboxd/assets/33938646/c63743a3-515b-42ca-bba7-c381b75b480d) 60 | 61 | - Dalej, to samo co w "Obejrzane filmy" 62 | - "Chcę zobaczyć" seriale: 63 | - Analogicznie do "Obejrzane seriale" i "Chcę zobaczyć filmy" 64 | 65 | ## Uwagi 66 | 67 | - Z powodu ograniczenia rozmiaru pliku, narzuconego przez importer Letterboxd, skrypt dzieli obejrzane filmy/seriale na kilka plików (max 1800 wierszy). 68 | Jeśli masz ponad 1800 filmów, a tylko jeden plik się ściąga, sprawdź ten issue i komentarz [https://github.com/JSerwatka/Filmweb2Letterboxd/issues/20#issuecomment-3310788462](https://github.com/JSerwatka/Filmweb2Letterboxd/issues/20#issuecomment-3310788462) 69 | - Częste zmiany na stronie Filmweb'u powodują, że skrypty i API szybko stają się nieaktualne. Gdyby tak się stało, można śmiało zgłaszać swoje _PR_ lub _Issues_. 70 | 71 | ## Letterboxd Tweaks 72 | Stworzyłem również rozszerzenie do przeglądarek opartych na Chromium (Chrome, Edge, Brave) i Firefox, które znacząco podnosi komfort korzystania z serwisu Letterboxd. 73 | - Rozszerzenie jest dostępne [tutaj](https://chromewebstore.google.com/detail/letterboxd-tweaks/hopfbphfhmjgdnedoldfpbhepohibfkj) 74 | - Kod projektu znajduje się w [tym repo](https://github.com/JSerwatka/letterboxd-tweaks) 75 | --------------------------------------------------------------------------------