Okay, am looking now and it started failing since the API changes (@tofugu-scott)
Specifically it’s failing on this segment:
TypeError: Cannot read properties of undefined (reading 'srsStages')
at [unknown function](Code:171:29)
at generateDataFromAssignments(Code:168:21)
at generateNewDailyEntry_(Code:251:3)
Which is this line:
counts[subject_type]['srsStages'][srs_stage]++
I suspect it’s iterating over subject_types and is getting an extra response and therefore failing.
It’s a pain debugging these scripts, but I suspect the changes need to be like this:
Replace likes 12 to 55 with this:
headers: [ 'Date (YYYY-MM-DD)', 'Apprentice radicals', 'Apprentice kanji', 'Apprentice vocabulary', 'Apprentice kana vocabulary', 'Guru radicals', 'Guru kanji', 'Guru vocabulary', 'Guru kana vocabulary', 'Master radicals', 'Master kanji', 'Master vocabulary', 'Master kana vocabulary', 'Enlightened radicals', 'Enlightened kanji', 'Enlightened vocabularies', 'Enlightend kana vocabularies', 'Burned radicals', 'Burned kanji', 'Burned vocabularies', 'Burned kana vocabularies', 'Unlocked radicals', 'Unlocked kanji', 'Unlocked vocabulary', 'Unlocked kana vocabulary', 'Started radicals', 'Started kanji', 'Started vocabulary', 'Started kana vocabulary', 'Passed radicals', 'Passed above kanji', 'Passed above vocabulary', 'Passed kana vocabulary', 'Burned (event) radicals', 'Burned (event) kanji', 'Burned (event) vocabulary', 'Burned (event) kana vocabulary', 'Resurrected radicals', 'Resurrected kanji', 'Resurrected vocabulary', 'Resurreced kana vocabulary', 'Lesson radicals', 'Lesson kanji', 'Lesson vocabulary', 'Lesson kana vocabulary', 'Review radicals', 'Review kanji', 'Review vocabulary', 'Review kana vocabulary', 'Total available radicals', 'Total available kanji', 'Total available vocabulary', 'Total available kana vocabulary', 'Current level', 'Days on current level', ],
Add this on like 173, after the },
kana_vocabulary: {
lessons: 0,
reviews: 0,
unlocked: 0,
started: 0,
passed: 0,
burned: 0,
resurrected: 0,
totalAvailable: 0,
srsStages: {
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
}
},
Alternately, here’s the whole script:
const onOpen = () => {
const spreadsheet = SpreadsheetApp.getActive()
const menuItems = [
{ name: 'Prepare sheets...', functionName: 'prepareSheets_' },
{ name: 'Add new daily entry...', functionName: 'generateNewDailyEntry_' }
]
spreadsheet.addMenu('Daily Status', menuItems)
}
const dailyEntriesSheetDetails = {
name: 'Daily Entries',
headers: [
'Date (YYYY-MM-DD)',
'Apprentice radicals',
'Apprentice kanji',
'Apprentice vocabulary',
'Apprentice kana vocabulary',
'Guru radicals',
'Guru kanji',
'Guru vocabulary',
'Guru kana vocabulary',
'Master radicals',
'Master kanji',
'Master vocabulary',
'Master kana vocabulary',
'Enlightened radicals',
'Enlightened kanji',
'Enlightened vocabularies',
'Enlightend kana vocabularies',
'Burned radicals',
'Burned kanji',
'Burned vocabularies',
'Burned kana vocabularies',
'Unlocked radicals',
'Unlocked kanji',
'Unlocked vocabulary',
'Unlocked kana vocabulary',
'Started radicals',
'Started kanji',
'Started vocabulary',
'Started kana vocabulary',
'Passed radicals',
'Passed above kanji',
'Passed above vocabulary',
'Passed kana vocabulary',
'Burned (event) radicals',
'Burned (event) kanji',
'Burned (event) vocabulary',
'Burned (event) kana vocabulary',
'Resurrected radicals',
'Resurrected kanji',
'Resurrected vocabulary',
'Resurreced kana vocabulary',
'Lesson radicals',
'Lesson kanji',
'Lesson vocabulary',
'Lesson kana vocabulary',
'Review radicals',
'Review kanji',
'Review vocabulary',
'Review kana vocabulary',
'Total available radicals',
'Total available kanji',
'Total available vocabulary',
'Total available kana vocabulary',
'Current level',
'Days on current level',
],
}
const apiSheetDetails = {
name: 'API',
headers: [
'API token (version 2)'
],
}
const prepareSheets_ = () => {
prepareSheet_(dailyEntriesSheetDetails)
prepareSheet_(apiSheetDetails)
getSheetByName_(apiSheetDetails.name).getRange('A2').setValue('(Replace this cell with your API token)')
}
const prepareSheet_ = ({name, headers}) => {
const sheet = insertSheet_().setName(name)
if (sheet.getMaxColumns() < headers.length) {
sheet.insertColumnsAfter(sheet.getMaxColumns() - 1, headers.length - sheet.getMaxColumns())
}
if (sheet.getMaxColumns() > headers.length) {
sheet.deleteColumns(headers.length, sheet.getMaxColumns() - headers.length)
}
sheet.deleteRows(2, sheet.getMaxRows() - 2)
sheet.getRange("A1:1").setValues([headers]).setFontWeight('bold')
sheet.setFrozenRows(1)
sheet.autoResizeColumns(1, headers.length)
}
const generateNewDailyEntry_ = () => {
const level = {
current: null,
numberOfDaysOnCurrent: null
}
const counts = {
radical: {
lessons: 0,
reviews: 0,
unlocked: 0,
started: 0,
passed: 0,
burned: 0,
resurrected: 0,
totalAvailable: 0,
srsStages: {
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
}
},
kanji: {
lessons: 0,
reviews: 0,
unlocked: 0,
started: 0,
passed: 0,
burned: 0,
resurrected: 0,
totalAvailable: 0,
srsStages: {
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
}
},
vocabulary: {
lessons: 0,
reviews: 0,
unlocked: 0,
started: 0,
passed: 0,
burned: 0,
resurrected: 0,
totalAvailable: 0,
srsStages: {
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
}
},
kana_vocabulary: {
lessons: 0,
reviews: 0,
unlocked: 0,
started: 0,
passed: 0,
burned: 0,
resurrected: 0,
totalAvailable: 0,
srsStages: {
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
}
},
}
const generateDataFromAssignments = () => {
let assignmentsEndpoint = 'https://api.wanikani.com/v2/assignments?hidden=false'
while (assignmentsEndpoint) {
const response = fetchData_(assignmentsEndpoint)
response.data.forEach(({ data }) => {
const { subject_type, srs_stage, unlocked_at, started_at, passed_at, burned_at, resurrected_at } = data
counts[subject_type]['srsStages'][srs_stage]++
if (unlocked_at) { counts[subject_type]['unlocked']++ }
if (started_at) { counts[subject_type]['started']++ }
if (passed_at) { counts[subject_type]['passed']++ }
if (burned_at) { counts[subject_type]['burned']++ }
if (resurrected_at) { counts[subject_type]['resurrected']++ }
if (unlocked_at && !started_at) { counts[subject_type]['lessons']++ }
if (started_at && (!burned_at || resurrected_at)) { counts[subject_type]['reviews']++ }
})
assignmentsEndpoint = response.pages.next_url
}
}
const generateDataFromSubjects = () => {
let subjectsEndpoint = 'https://api.wanikani.com/v2/subjects?hidden=false'
while (subjectsEndpoint) {
const response = fetchData_(subjectsEndpoint)
response.data.forEach(({ object }) => {
counts[object]['totalAvailable']++
})
subjectsEndpoint = response.pages.next_url
}
}
const generateDataFromLevelProgressions = () => {
let levelProgressionsEndpoint = 'https://api.wanikani.com/v2/level_progressions'
let levelProgressions = []
while (levelProgressionsEndpoint) {
const response = fetchData_(levelProgressionsEndpoint)
response.data.forEach(({ data }) => {
levelProgressions = [...levelProgressions, data]
})
levelProgressionsEndpoint = response.pages.next_url
}
const latestLevelProgression = levelProgressions[levelProgressions.length - 1]
if (latestLevelProgression) {
level.current = latestLevelProgression.level
level.numberOfDaysOnCurrent = numberOfDaysBetweenDates(new Date(latestLevelProgression.unlocked_at), new Date())
}
}
const srsStagesByStageName = {
apprentice: [1, 2, 3, 4],
guru: [5, 6],
master: [7],
enlightened: [8],
burned: [9],
}
const todayDate_ = () => Utilities.formatDate(new Date(), getActiveSpreadsheet_().getSpreadsheetTimeZone(), 'yyyy-MM-dd')
const numberOfDaysBetweenDates = (earlyDate, laterDate) => (laterDate.getTime() - earlyDate.getTime()) / (1000 * 3600 * 24)
const totalSrsStagesCountBySubjectType_ = (counts, srsStages, subjectType) => {
return srsStages.reduce((totalCount, srsStage) => {
return totalCount + counts[subjectType]['srsStages'][srsStage]
}, 0)
}
const fetchData_ = (apiEndpoint) => {
const response = UrlFetchApp.fetch(apiEndpoint, {
method: 'GET',
headers: {
'Authorization': `Bearer ${getApiToken_()}`,
'Content-Type': 'application/json',
},
})
return JSON.parse(response.getContentText())
}
generateDataFromAssignments()
generateDataFromSubjects()
generateDataFromLevelProgressions()
getSheetByName_(dailyEntriesSheetDetails.name).appendRow([
todayDate_(),
...Object.keys(srsStagesByStageName).map((srsStageName) => {
return Object.keys(counts).map((subjectType) => totalSrsStagesCountBySubjectType_(counts, srsStagesByStageName[srsStageName], [subjectType]))
}).flat(),
...['unlocked', 'started', 'passed', 'burned', 'resurrected', 'lessons', 'reviews', 'totalAvailable'].map((countType) => {
return Object.keys(counts).map((subjectType) => counts[subjectType][countType])
}).flat(),
level.current,
level.numberOfDaysOnCurrent,
])
}
const getActiveSpreadsheet_ = () => SpreadsheetApp.getActiveSpreadsheet()
const getApiToken_ = () => getSheetByName_(apiSheetDetails.name).getRange('A2').getValue()
const getSheetByName_ = (sheetName) => getActiveSpreadsheet_().getSheetByName(sheetName)
const insertSheet_ = () => getActiveSpreadsheet_().insertSheet()
Hopefully that’ll work, but I can’t confirm right now.