pirxpilot / liftie

:ski: Clean, simple, easy to read, fast ski resort lift status.
https://liftie.info
BSD 3-Clause "New" or "Revised" License
67 stars 31 forks source link

Cannot read properties of undefined (reading 'foreEach') #57

Closed JeppePape closed 12 months ago

JeppePape commented 1 year ago

Index.js

var debug = require('debug')('liftie:resort:bad-kleinkirchheim'); module.exports = parse;

function parse(data) { var liftStatus = {};

data.lifts.forEach(function(lift) { liftStatus[lift.popup.title.trim()] = lift.status; });

debug('bad-kleinkirchheim lift status:', liftStatus); return liftStatus; }

Resort.json

{ "name": "bad-kleinkichheim", "url": { "host": "https://winter.intermaps.com", "pathname": "/bad_kleinkirchheim/data?lang=en" }, "api": { "host": "https://winter.intermaps.com", "pathname": "/bad_kleinkirchheim/data?lang=en" }, "tags": [ "austria" ], "ll": [ null ] }

I know that i shouldnt have to put in the api i like this, when it isnt an api link, however i was just testing out if data?lang=en or api might have been the issue.

JeppePape commented 1 year ago

As the title suggest. I have about 40-50 ski resorts in the EU connected to the winter.intermaps.com websites. The index methods are completely identical for all of them and they all work with make. However about half of them return this specific error and doesnt pop up on localhost with the information. I simply am not able to understand the specific reason why this is happening, but i am thinking if any of the rate or time limiters could be a factor.

Also is there any way to change the time from every 60seconds to 5 minutes for example ?

pirxpilot commented 1 year ago

You can change the interval here

var interval = 60 * 1000; // once a minute - fetch status if there were requests

It should probably be configurable through environment variable. PRs always welcomed ;-) I can really comment or help on the root issue though. Definitely not without the code that you added and a way to reproduce it.

JeppePape commented 1 year ago

You can change the interval here

var interval = 60 * 1000; // once a minute - fetch status if there were requests

It should probably be configurable through environment variable. PRs always welcomed ;-) I can really comment or help on the root issue though. Definitely not without the code that you added and a way to reproduce it.

i suppose that PRs are "push requests" ? :)

But yeah otherwise i havent really added anything special that hasnt been done in all the other ones on the readme. I always saw this error happen here and there. However after letting it sit for a while it seems that even though the error comes up, it now does give info for nearly all of my resorts. So i ma thinking it might be an issue with the data it receives from the resorts in a particular interval that could be corrupt or wrong. update. It actually seems that the structure received from the website is a text looking like this

Text { parent: Document { parent: null, prev: null, next: null, startIndex: null, endIndex: null, children: [Circular *1], type: 'root' }, prev: null, next: null, startIndex: null, endIndex: null, data: .... }

where data has a json struct within it.

If you would like to try and replicate the problem i am having simply use the ./generate with the url https://winter.intermaps.com/berwang/data?lang=en and name diedamskopf

and then use the index.js code specified here

var debug = require('debug')('liftie:resort:diedamskopf'); module.exports = parse; function parse(data) { var liftStatus = {};

data.lifts.forEach(function(lift) { liftStatus[lift.popup.title.trim()] = lift.status; }); debug('diedamskopf lift status:', liftStatus); return liftStatus; }

And thanks for the interval location, i didnt seem to be able to find it, i suppose it does a check every interval and if it returns 304, nothing is retrieved and then on a long interval it retrieves everything anyway, even if it is a 304 ?

sry i cant be of much more help PR´s and html scraping is a bit new to me :)

And i also made some custom changes to the templates, so not a good idea to do a PR, as it wouldnt make the generate function work correctly for all other websites but the specific ones i am working with returning json structures atm i suppose ?

JeppePape commented 1 year ago

As a sidenote, does the examples get redownloaded every 30 mins as well or do they only get downloaded on generation ?

The reason i am asking this, is whether Make can be used to actually test if all the websites function correctly using make as a Test that is run every night to confirm, but this would only work if the example html and json files are actually retrieved more than just once ?

pirxpilot commented 1 year ago

You need to run:

bin/fetch <resort>

to update example.

JeppePape commented 1 year ago

You need to run:

bin/fetch <resort>

to update example.

are you able to replicate the issue, if you add the resort to your own running instance ?

JeppePape commented 1 year ago

You need to run:

bin/fetch <resort>

to update example.

I just made a pull request, hope that it can be used, but i had to revert some changes to the local copy i had and make a fork instead so i could push some of the resorts i made. A lot of the remaining resorts are getting both lifts and pistes and i have made custom changes to the template files, so i didn't want to add all of that without knowing if it was of any interest to the liftie's vision.

As a sidenote, have you ever tried making multiple parse or module exports within one file ?

I am currently trying to make it work (as i am trying to get both lifts and slopes in one list), where both work separately, however it doesn't work, when bundled together.

module.exports = {
  parseStatements: [
{
  selector: '.snow-report-terrain .ab-status-large',
  parse: 
  {
    name: 1,
    status: {
      child: 0,
      attribute: 'class',
      fn: v => v.split('_').pop()
    }
  }
},
{
    selector: 'div.dynamicCodeInsert.sortableModule.snow-report-terrain.no-edit-bar > div > button > div.ab-status_sub > div',
    parse: 
    {
      name: 2,
      status: {
        child: 0,
        attribute: 'class',
        fn: text => {
          if (text.includes("open")) {
            return 'open';
          }
          return 'closed';
        }
      }
    }
  }
]
};

I solved this issue of multiple parses by modifying the parse.js file as such.

 function collectParse(dom) {
    // Check if descriptor equals parseConfig1 and parseConfig2 for combination of several selectors
    if (descriptor.parseConfig1 && descriptor.parseConfig2) {
      // Collect liftStatus for parseConfig1
      const liftStatus1 = domutil.collect(dom, descriptor.parseConfig1.selector, descriptor.parseConfig1.parse);
      const liftStatus2 = domutil.collect(dom, descriptor.parseConfig2.selector, descriptor.parseConfig2.parse);
      // Combine liftStatus1 and liftStatus2 into a single object
      const liftStatus = {
        // Merge properties as needed
        ...liftStatus1,
        ...liftStatus2,
      };
      debug(`${descriptor.resortId} Combined Lift Status:`, liftStatus);
      return liftStatus;
    } else {
      // Proceed as usual
      const liftStatus = domutil.collect(dom, descriptor.selector, descriptor.parse);
      debug(`${resortId} Lift Status:`, liftStatus);
      return liftStatus;
    }
  }

and parsing it as such in the indexfile, keeping the original functionality but adding the functionality of retrieving several different parses on a website. Essentially you can modify it even more with more if statements according to any additional features you would like alongside your lifts from that one website.


module.exports = {
  parseConfig1: {
    selector: '.snow-report-terrain .ab-status-large',
    parse: {
      name: 1,
      status: {
        child: 0,
        attribute: 'class',
        fn: v => v.split('_').pop()
      }
    }
  },
  parseConfig2: {
    selector: 'div.dynamicCodeInsert.sortableModule.snow-report-terrain.no-edit-bar > div > button > div.ab-status_sub > div',
    parse: {
      name: 2,
      status: {
        child: 0,
        attribute: 'class',
        fn: text => {
          if (text.includes("open")) {
            return 'open';
          }
          return 'closed';
        }
      }
    }
  }
};