עזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS)
-
המטרה - לקבל מערך של מזהי סרטונים בערוץ מסוים ביוטיוב (לפי מזהה ערוץ) דרך הAPI של גוגל.
גוגל מאפשרת לקבל בכל פעם עד 50 תוצאות.
אם יש יותר - הAPI מחזיר ערךnextPageToken
, שאותו מעבירים בפרמטרpageToken
בשאילתה החדשה, וכן הלאה עד שלא חוזר nextPageToken ואז זה אומר שאין עוד תוצאות.
בעצם מה שניסיתי לממש זה את המדריך הזה:
http://truelogic.org/wordpress/2017/06/20/7-youtube-data-api-paging-maxresults
כתבתי את הקוד הבא, שבעצם מבצע קריאה ראשונית, מקבל את התוכן ודוחף את מזהי הוידאו למערך "arryYT" ואז באם חוזר פרמטר nextPageToken מבצע שאילתה נוספת, מכניס למערך, ובאם עדיין יש nextPageToken מבצע שוב קריאה, וכן הלאה.
let arryYT = [] async function lulaa(ChannelID) { console.log("run") let a1 = await fetch(`https://www.googleapis.com/youtube/v3/search?key=` + TokenAPI() + `&channelId=${ChannelID}&part=id&order=date&maxResults=50`) a1 = await a1.json() console.log(Math.ceil(a1.pageInfo.totalResults / 50)) a1.items.forEach((parit, index) => { if (parit.id.kind === "youtube#video") { arryYT.push(parit.id.videoId) } }) if (a1.nextPageToken) { getNextPage(a1.nextPageToken) } if (a1.nextPageToken) { getNextPage(a1.nextPageToken) } async function getNextPage(nextPageToken) { console.log("run") let a1 = await fetch(`https://www.googleapis.com/youtube/v3/search?key=` + TokenAPI() + `&channelId=${ChannelID}&part=id&order=date&maxResults=50&pageToken=${nextPageToken}`) a1 = await a1.json() a1.items.forEach((parit, index) => { if (parit.id.kind === "youtube#video") { arryYT.push(parit.id.videoId) } }) } if (a1.nextPageToken) { getNextPage(a1.nextPageToken) } } function TokenAPI() { return "AIzaSyBUtCAp82VCsH5z3XmTMy1KMnLQ1g3Cqm0" }
השאלה איך אני יודע שכל הקריאות (וניתוח הנתונים) הסתיימו בהצלחה? כדי להמשיך לשלב הבא - שליחת המערך המוכן של המזהים לפונקציה אחרת שתנתח אותם.
כמו כן, נראה שבכלל לא כל הנתונים מתקבלים...
בקריאה לדוגמה לערוץ הזה:lulaa("UCzlw5vTVVMPwBKElMt3gYQw")
שיש 399 פריטים שבשביל לקבל אותם צריך 8 דפי מידע (בשורה 6 מודפס לקונסול המידע הזה), אבל בפועל הקונסולוגים שהכנסתי בשורות 19 ו3 מראים שזה רץ רק 4 פעמים, והתקבל מערך עם 195 פריטים בלבד (המספר בניקוי הפלייליסטים (שורה 8/23), ועדיין...).
גם כשחיכיתי עוד קצת, והדפסתי את המערך, לא היו בו את כל תוצאות נוספות...
הפונקציה TokenAPI במקור מחזירה מפתח API לפי מה שהמשתמש הגדיר (נשמר בlocalStroage) וכו', בשביל הדוגמה עשיתי שהיא תחזיר מפתח קבוע...
אשמח לעזרה.מקווה שזה מספיק ברור, כתבתי כבר את הפוסט והשקעתי זמן רב בניסוחו ועקב תקלה טכנית הוא נמחק, בתקווה להבנה...
-
@צדיק-תמים אני זוכר שעבדתי פעם עם ה API שלהם ונתקלתי בבעיה הזו, קראתי על זה גם איפשהו בסטאק, והפתרון היחיד שמצאתי זה לא לסמוך על ה nextPageToken אלא לעשות את הסינון ידנית עם publishedBefore publishedAfter, כלומר אחרי שאתה מקבל 50 תוצאות בדף הראשון, אתה הולך לתוצאה האחרונה, בודק תאריך פרסום שלה, ובבקשה הבאה אתה מבקש את התוצאות שפורסמו לפני התאריך הזה, כך זה נותן לך את ה 50 תוצאות הבאות אחריה.
אבל זה הופך את הפונקציה ליותר מסובכת. -
@צדיק-תמים אתה יכול לבדוק את זה בצורה ויזואלית, לפתוח דפדפן רגיל, ולהכנס לכתובת של ה API עם הפרמטר של nextPageTOken ואז תוכל לראות איפה זה נעצר וכבר אין תוצאות יותר, ולהשוות את זה עם הסרטים של הערוץ באמת
-
@יוסף-בן-שמעון עד 500 תוצאות זה כן אמור להיות מדויק בצורה הזאת, ולי זה מספיק, הקוד לא מתוכנן כרגע לעבוד על ערוצים יותר גדולים מזה.
מלבד זאת, גם אם אני יעשה את זה ככה, עדיין נשארת השאלה שבשבילה פתחתי את הנושא - איך אני יודע שכל הקריאות הסתיימו?
וכן כשאני נכנס ידנית (בדפדפן) לכתובות עם הpageToken, אני כן מצליח לקבל את כל התוכן, ככה שנראה שזה בעיה בקוד. -
@צדיק-תמים הקוד לא כל כך מובן לי (אולי גם לך)
יש שלוש קריאות לפונקצייתgetNextPage
לכאורה לכן אתה מקבל סה"כ 4 דפים של תוצאות.
במקום כמה קריאות, אמור להיות שם לולאה שתנאי העצירה שלה זה אם לא התקבלnextPageToken
גם, חסרawait
לפני הקריאה ל-getNextPage
-
@yossiz אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
במקום כמה קריאות, אמור להיות שם לולאה שתנאי העצירה שלה זה אם לא התקבל nextPageToken
יפה! לא חשבתי על הכיוון הזה, (בעצם יצרתי פונקציה שקוראת לעצמה [בתנאי מסוים]). לכאורה הכי מתאים זה
do while
.
עכשיו נשארת השאלה איך אני יודע שהכל הסתיים? -
@צדיק-תמים אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
בעצם יצרתי פונקציה שקוראת לעצמה
לפי הקוד שהבאת למעלה, זה לא מה שעשית. הפונקציה
getNextPage
לא קוראת לעצמה בקוד הנ"ל.לכאורה הכי מתאים זה do while.
יפה
עכשיו נשארת השאלה איך אני יודע שהכל הסתיים?
אם אתה עושה await על כל קריאה בלולאה, אתה יודע שהכל הסתיים ביציאה מהלולאה
-
@צדיק-תמים התקשיתי להבין את הקוד שלך ומשום מה התחשק לי לארגן את זה מחדש (refactoring בלע"ז). זה מה שיצא לי (מקוה שיהיה לתועלת...):
const API_TOKEN = 'AIzaSyBUtCAp82VCsH5z3XmTMy1KMnLQ1g3Cqm0'; const YOUTUBE_SEARCH_ENDPOINT = 'https://www.googleapis.com/youtube/v3/search'; async function fetchVideoIds (channelId, pageToken = null) { const endPoint = getYouTubeEndpoint(channelId, pageToken); const response = await fetch(endPoint).then((res) => res.json()); const videoList = response.items.filter((item) => item.id.kind === 'youtube#video'); const videoIdList = videoList.map((item) => item.id.videoId); const nextPageToken = response.nextPageToken; return { videoIdList, nextPageToken }; } function loop () { const list = []; // recursive function async function getVideoIdList (pageToken) { const CHANNEL_ID = 'UCzlw5vTVVMPwBKElMt3gYQw'; console.log('Fetching...'); const { videoIdList, nextPageToken } = await fetchVideoIds(CHANNEL_ID, pageToken); list.push(...videoIdList); if (!nextPageToken) { return list; } return getVideoIdList(nextPageToken); } return getVideoIdList(); } // trigger the function loop().then((result) => console.log(result)); // helper function function getYouTubeEndpoint (channelId, nextPageToken) { const endPoint = `${YOUTUBE_SEARCH_ENDPOINT}?key=${API_TOKEN}&channelId=${channelId}&part=id&order=date&maxResults=50`; if (nextPageToken) { return `${endPoint}&pageToken=${nextPageToken}`; } return endPoint; }
אני מסתפק אם יותר נכון לממש את הלולאה עם
while
במקום הרקורסיה. כמובן, שתמיד אפשר לשפר עוד...
מראה מקומות:
טיפ כללי: תמיד אחרי שהקוד עובד והמחשב כבר מבין מה אתה רוצה, שוה לעצור ולכתוב מחדש את הכל כדי שגם אתה תבין מה כתוב בפעם הבא כשאתה מסתכל על הקוד ("פעם הבא" יכול להיות גם מחר...).
-
@shaya קודם כל את הקוד ההוא באמת גם לי היה קשה להבין...
אני צריך למצוא זמן יותר מתאים לקרוא בעיון את הקוד שלך (והמראי מקומות), אבל למעשה כתבתי את הקוד הזה:
שאמנם הוא חסר את הסדר שלך (למשל בהגדרתYOUTUBE_SEARCH_ENDPOINT
שמקצר את שורת הקריאה בפועל), אבל הוא קצר בהרבה מהקוד שלך (וכמובן פועל היטב).
אני אישית סובר שקוד טוב הוא קוד חסכני (כמובן שמשיג את אותה תוצאה, ובצורה שברורה לי).async function lulaa(ChannelID) { let arryYT = [] let a1 = { nextPageToken: "", } let artToken = "" do { a1 = await fetch(`https://www.googleapis.com/youtube/v3/search?key=` + TokenAPI() + `&channelId=${ChannelID}&part=id&order=date&maxResults=50${artToken}${a1.nextPageToken}`) a1 = await a1.json() a1.items.forEach((parit, index) => { if (parit.id.kind === "youtube#video") { arryYT.push(parit.id.videoId) } }) artToken = "&pageToken=" } while (await a1.nextPageToken) console.log(arryYT) } function TokenAPI() { return "AIzaSyBUtCAp82VCsH5z3XmTMy1KMnLQ1g3Cqm0" }
(הקוד הזה עצמו לא יעבוד, כי המפתח API הזה לא עובד כרגע, כי נגמרה המכסה היומית... שימוש בsearch לוקח 100 יחידות שימוש... צריך לבדוק אם ואיך ניתן לקבל את הנתונים בצורה רגילה. בכל מקרה עם מפתח אחר זה עובד היטב)
-
@צדיק-תמים אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
אני אישית סובר שקוד טוב הוא קוד חסכני (כמובן שמשיג את אותה תוצאה, ובצורה שברורה לי).
קיבלת עצה זהב ממקצוען, וזו לא רק דעתו האישית, זו מוסכמה רווחת ותקן סטנדרטי בענף, אל תחסוך בקוד גם אם נדמה לך שזה חסכוני, כי זה בסוף הרבה יותר יקר..
אגב גם בשמות משתנים אל תחסוך, שם משתנה a1 זה לא תקין בעליל, גם העובדה שהוא משמש לכמה דברים בזה אחר זה זו עבירה פלילית,
-
@יוסף-בן-שמעון אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
שם משתנה a1 זה לא תקין בעליל
אכן,
זה לא אמור להישאר ככה, זה רק בשביל כתיבת עצם הקוד, ככה יותר נוח.
למעשה אח"כ אני משכתב את שמות המשתנים והפונקציות@יוסף-בן-שמעון אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
הוא משמש לכמה דברים בזה אחר זה
איזה דברים?
אני מגדיר אותו קודם כל כמערך עם מחרוזת ריקה בnextPageToken, בשביל תקינות הקריאה הראשונה.
חוץ מזה אין עוד שימוש -
@צדיק-תמים אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
@יוסף-בן-שמעון אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
הוא משמש לכמה דברים בזה אחר זה
איזה דברים?
אני מגדיר אותו קודם כל כמערך עם מחרוזת ריקה בnextPageToken, בשביל תקינות הקריאה הראשונה.
חוץ מזה אין עוד שימושקודם הוא אובייקט שמחזיק מחרוזת ריקה, אחר כך הוא עובר להיות הפרומיס של הבקשה, ואחר כך הגייסון של ניתוח הבקשה.
(אגב אפשר לאחד את הפרומיס שיחזיר גייסון כמו שהראה @Shaya בדוגמא שלו, אין צורך לפצל את זה לשתי שורות)אתה ניצב מול בעיה, איך לבצע את הבקשה הראשונה בלי להכשל, וכדי להתמודד איתה אתה יוצר מעקף ע"י משתנה עם מחרוזת ריקה, זה מעקף, זה לא הדרך הנכונה, הדרך הנכונה היא להסתכל לבעיה בעיניים ולפתור אותה מהשורש
יש דרך לבנות URL בצורה יפה אלגנטית וקריאה, יש מחלקה בשם URL שלוקחת פרמטרים ומחזירה URL תקין עם פרמטרי חיפוש ושאר ירקות, יותר נכון לעבוד איתה למרות שזה כאילו מאריך את הקוד, אבל זה קריא, וזה קל לתחזוקה
-
@יוסף-בן-שמעון אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
יש מחלקה בשם URL שלוקחת פרמטרים ומחזירה URL תקין עם פרמטרי חיפוש ושאר ירקות
אשמח להרחבה, ובכלל הייתי שמח להעמיק קצת יותר בכל העניין של כתיבה נכונה של קוד (או לפחות בעניינים ש @Shaya נגע)... זה אכן דבר חשוב.
אולי @dovid יוכל לפצל את הפוסטים האחרונים לנושא חדש? -
@צדיק-תמים אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
אשמח להרחבה
תראה לדוגמא את הקוד הזה
const url = new URL('https://www.googleapis.com/youtube/v3/search'); url.searchParams.append('channelId', channelId); if (nextPageToken) url.searchParams.append('pageToken', nextPageToken); console.log(url.toString());
שרשור מחרוזות זה לא דבר קל, ובפרט אם תרצה אחר כך לתחזק את זה (להוסיף תוכן למחרוזת, לשנות תנאים וכדומה) זה הופך להיות סיוט.
במקום זה אתה שולט על ה URL עם קוד טהור, מוסיף פרמטרים עם המתודה append, ובסוף אתה מדפיס את התוצאה עם toString@צדיק-תמים אמר בעזרה - שרשור פרומיסים מותנה וביצוע פעולה רק בסיום כולם (JS):
ובכלל הייתי שמח להעמיק קצת יותר בכל העניין של כתיבה נכונה של קוד
הבסיס הוא כפי שכתב שעייה, תחשוב איך אדם אחר יבין את הקוד שלך (או אתה עצמך עוד חצי שנה), הקוד אמור להסביר את עצמו, שכל מתכנת יבין מה התפקיד של המשתנה הזה ולמה אתה קורא עכשיו לפונקציה הזו ומה אתה מצפה ממנה להחזיר, לכן חשוב להקפיד על שמוות משתנים ברורים, לייחד לכל פונקציה שם ברור ומטרה ברורה, לא לחסוך בקוד על חשבון הקריאות, גם אם נדמה לך שזה יעיל יותר וקצר יותר.
לדוגמא מה זה הפונקציה בשם "לולאה", לא ברור מה המטרה שלה, צריך לקרוא לה שם שיבהיר שהיא מקבלת מידע על הערוץכדאי מאד גם להצמד לתקנים אוניברסליים, לדוגמא פונקציה שמחזירה ערך אמורה להתחיל ב get, כמו getToken ופונקציה שאמורה להגדיר ערך אמורה להתחיל ב set, מחלקה מתחילה באות גדולה, פונקציה מתחילה באות קטנה, כל הדברים האלה עוזרים להבנת הקוד
-
@יוסף-בן-שמעון כמו שכתבתי קודם השמות הם זמניים, יותר נוח לי להכין את הקטע קוד (כשמדובר בקטע מורכב ולא שאני כותב וזהו) עם שמות זמניים וקצרים שיותר מובנים לי במשמעות של הקטע הספציפי, ולאחר מכן כשאני משלב את זה בסקריפט אני נותן לזה מחדש שמות לפי המשמעות ביחס לפעולה הכללית של הסקריפט.
לגבי המחלקה URL והדוגמה שהבאת - זה נראה ממש מעולה, לא הכרתי את זה קודם (וגם כמדומני לא נתקלתי בזה בסקריפטים וקטעי קוד אחרים ברשת).
ולגבי העניין של להגדיר משתנה מחדש - אז לי אישית יותר קשה להבין קוד שלוקח נתון ומכניס אותו לתוך משתנה x, ואז מוטציה על x שמוכנסת לy, וכן הלאה, כשלמעשה בסופו של דבר משתמשים רק במוטציה האחרונה - לדוגמה y, אז מה העניין בעצם 'לשרשר' את זה? כל עוד אני צריך רק את התוצאה הסופית, למה לא לערוך את המשתנה הראשוני ולהכניס בו את התוכן לאחר העריכה, ולהשתמש בו?
במראה מקום הזה ששעייה הביא, בעצם כתוב שיש עניין ליצור למשל את האובייקט כבלתי ניתן לשינוי, אבל לא הבנתי מה מרוויחים מזה באמת? -
@יוסף-בן-שמעון הנה קטע קוד סופי לדוגמה:
/** * קבל מזהה סרטון, החזר שם סרטון, שם ערוץ, ומזהה ערוץ * @param {string} VideoID מזהה סרטון יוטיוב */ async function getChannelForVideoID(VideoID) { const GET_INFO_FOR_VIDEO_ID = new URL("https://www.googleapis.com/youtube/v3/videos") GET_INFO_FOR_VIDEO_ID.searchParams.append("id", VideoID) GET_INFO_FOR_VIDEO_ID.searchParams.append("key", getTokenAPI()) GET_INFO_FOR_VIDEO_ID.searchParams.append("part", "snippet") GET_INFO_FOR_VIDEO_ID.searchParams.append("fields", "items(snippet(channelTitle,channelId,title))") console.log(GET_INFO_FOR_VIDEO_ID.toString()) const infoVideo_Raw = await fetch(GET_INFO_FOR_VIDEO_ID) let infoVideo_Object = await infoVideo_Raw.json() let videoINFO = [infoVideo_Object.items[0].snippet.channelId, infoVideo_Object.items[0].snippet.channelTitle, infoVideo_Object.items[0].snippet.title] return videoINFO } console.log(await getChannelForVideoID("upjlMAKR-_0"))
זה מספיק ברור? או שיש עוד מה לשפר (כמובן אני לא שואל בשביל הקטע הזה ספציפית, אלא לידיעה כללית)
מתייג גם את @Shaya -
@צדיק-תמים ברור וקריא מאד
רק בקטנה, השם של המשתנה שבונה את ה URL לא אמור להיות GET_INFO_FOR_VIDEO_ID, כי זה לא פונקציה שמחזירה ערך, זה סה"כ URL חביב, צריך לקרוא לו כפשוטו url
ועוד משהו קטן, אין צורך לפצל את שורות 12 ו 13, זה יכול להיות פקודה אחת, כי אתה לא משתמש בפרומיס שמוחזר בשורה 12 אלא רק לצורך חילוץ הגייסון, אז זה יכול להיות כךconst infoVideo_Object = await fetch(GET_INFO_FOR_VIDEO_ID).then(res => res.json())