eawer
eawer

Reputation: 1458

How to send a variable from server to client in meteor?

I have a page with text input and a button. When I insert link to youtube video into text field and press the button - video downloads into the local folder.

The problem: how can I send link to local copy of the downloaded video back to the client?

More general question: How can I send a variable from server to client (this variable is temporary and is not going to be stored anywhere) ?

The code I have right now:

Client code

if (Meteor.isClient) {
    Path = new Meteor.Collection("path");
    Meteor.subscribe("path");

    Template.hello.events(
        {
            'submit .form' : function() {
                var link = document.getElementById("youtube-url").value;
                Meteor.call('download', link);
                event.preventDefault();
            }
        }
    );
}

Server code ('collection' part is not working)

if (Meteor.isServer) {
    Meteor.startup(function () {

        Meteor.methods({
            download: function (link) {
                var youtubedl = Npm.require('youtube-dl');
                var Fiber = Npm.require("fibers");
                var dl = youtubedl.download(link, './videos');

                // called when youtube-dl finishes
                dl.on('end', function(data) {
                  console.log('\nDownload finished!');
                  Fiber(function() { 
                      Path = new Meteor.Collection("path");
                      Path.insert({path: './videos/' + data.filename});
                  })
                });              
            }
        });
    });
}

Thanks!

Upvotes: 6

Views: 3056

Answers (4)

alykoshin
alykoshin

Reputation: 906

The answer to the question splits into 2 parts: (a) handling async functions inside Meteor's methods and (b) using youtube-dl package.

Async functions inside Meteor's methods

There are basically 2+ ways to work with async functions inside Meteor's methods: using future and using wrapAsync. If you look into Meteor's sources, you'll see that wrapAsync itself using future: https://github.com/meteor/meteor/blob/master/packages/meteor/helpers.js#L90. You can also use fibers directly, but it is not recommended.

Below are generic examples how to use them:

'use strict';

if (Meteor.isClient) {

   Template.methods.events({

    'click #btnAsync' : function() {
      console.log('Meteor.call(asyncMethod)');
      Meteor.call('asyncMethod', 1000, function(error, result) {
        if (error) {
          console.log('Meteor.call(asyncMethod): error:', error);
        } else {
          console.log('Meteor.call(asyncMethod): result:', result);
        }
      });
    },

    'click #btnFuture' : function() {
      console.log('Meteor.call(futureMethod)');
      Meteor.call('futureMethod', 1000, function(error, result) {
        if (error) {
          console.log('Meteor.call(futureMethod): error:', error);
        } else {
          console.log('Meteor.call(futureMethod): result:', result);
        }
      });
    },

    'click #btnFiber' : function() {
      console.log('Meteor.call(fiberMethod)');
      Meteor.call('fiberMethod', 1000, function(error, result) {
        if (error) {
          console.log('Meteor.call(fiberMethod): error:', error);
        } else {
          console.log('Meteor.call(fiberMethod): result:', result);
        }
      });
    }

  });

}

if (Meteor.isServer) {

  var demoFunction = function(duration, callback) {
    console.log('asyncDemoFunction: enter.');
    setTimeout(function() {
      console.log('asyncDemoFunction: finish.');
      callback(null, { result: 'this is result' });
    }, duration);
    console.log('asyncDemoFunction: exit.');
  };

  var asyncDemoFunction = Meteor.wrapAsync(demoFunction);

  var futureDemoFunction = function(duration) {
    var Future = Npm.require('fibers/future');
    var future = new Future();

    demoFunction(duration, function(error, result) {
      if (error) {
        future.throw(error);
      } else {
        future.return(result);
      }
    });
    return future.wait();
  };

  var fiberDemoFunction = function(duration) {
    var Fiber = Npm.require('fibers');
    var fiber = Fiber.current;

    demoFunction(duration, function(error, result) {
      if (error) {
        fiber.throwInto(new Meteor.Error(error));
      } else {
        fiber.run(result);
      }
    });

    return Fiber.yield();
  };

  Meteor.methods({

    asyncMethod: function (duration) {
      return asyncDemoFunction(duration);
    },
    futureMethod: function (duration) {
      return futureDemoFunction(duration);
    },
    fiberMethod: function (duration) {
      return fiberDemoFunction(duration);
    }

  });
}

You may also want look to Meteor.bindEnvironment() and future.resolver() for more complex cases.

Christian Fritz provided correct pattern for wrapAsync usage, however, during 2 years starting from the time the initial question was asked, the API of youtube-dl package has changed.

Using youtube-dl package

Because of changes in API, if you run his code, the server throws the exception visible in its console:

Exception while invoking method 'download' TypeError: Object function (videoUrl, args, options) {
...
} has no method 'download'

And Meteor returns to client undefined value:

here is the path: undefined

The code below is working (just replace downloadDir with your path) and returning filename to the client:

here is the path: test.mp4


File index.html

<head>
  <title>meteor-methods</title>
</head>
<body>
  {{> hello}}
</body>

<template name="hello">
  <form>
    <input type="text" id="youtube-url" value="https://www.youtube.com/watch?v=alIq_wG9FNk">
    <input type="button" id="downloadBtn" value="Download by click">
    <input type="submit" value="Download by submit">
  </form>
</template>

File index.js:

'use strict';

if (Meteor.isClient) {
  //Path = new Meteor.Collection("path");
  //Meteor.subscribe("path");

  Template.hello.events(
    {
      'submit .form, click #downloadBtn' : function() {
        var link = document.getElementById("youtube-url").value;

        //Meteor.call('download', link);
        Meteor.call('download', link, function(err, path) {
          if (err) { 
            console.log('Error:', err); 
          } else {
            console.log("here is the path:", path);
          }
        });

        event.preventDefault();
      }
    }
  );
}

if (Meteor.isServer) {

  var fs = Npm.require('fs');
  var youtubedl = Npm.require('youtube-dl');

  var downloadSync = Meteor.wrapAsync(function(link, callback) {
    var fname = 'test.mp4';
    // by default it will be downloaded to 
    // <project-root>/.meteor/local/build/programs/server/ 
    var downloadDir = './'; 
    console.log('\nStarting download...');

    // var dl = youtubedl.download(link, './videos');
    var dl = youtubedl(link, [], []);
    dl.on('info', function(info) {
      console.log('\nDownload started: ' + info._filename);
    });
    // dl.on('end', function(data) {
    dl.on('end', function() {
      console.log('\nDownload finished!');
      //callback(null, './videos/' + data.filename);
      callback(null, fname);
    });
    dl.on('error', function(error) {
      console.log('\nDownload error:', error);
      callback(new Meteor.Error(error.message) );
    });
    dl.pipe(fs.createWriteStream(downloadDir + fname));
  });

  Meteor.methods({
    download: function (link) {
      return downloadSync(link);
    }
  });

}

Current API does not allow to get youtube's filename when saving the file. If you want to save the file with the youtube's filename (as provided in initial question), you need to use getInfo() method of youtube-dl package.

Upvotes: 3

Martins Untals
Martins Untals

Reputation: 2298

You have to use async Future in Method definition, as explained in this answer. Then you will be able to wait with callback to client only after async download operation has finished

Upvotes: 0

Christian Fritz
Christian Fritz

Reputation: 21384

I think it would be much easier if you just returned the path you want from the method call. All you need to do to accomplish that is to make the youtube download synchronous -- which is sort of the meteor way of doing things.

This should work:

if (Meteor.isServer) {
    var youtubedl = Npm.require('youtube-dl');
    var sync = Meteor.wrapAsync(function(url, callback) {
        var dl = youtubedl.download(link, './videos');
        dl.on('end', function(data) {
            console.log('\nDownload finished!');
            callback(null, './videos/' + data.filename);
        });
    });

    Meteor.methods({
        download: function (link) {
            return sync(link);
        }
    });
}

Then, on the client, use:

Meteor.call('download', link, function(err, path) {
    console.log("here is the path:", path);
});

Upvotes: 0

Hubert OG
Hubert OG

Reputation: 19544

You can use this small package: https://atmosphere.meteor.com/package/client-call . It allows to call client-side methods from the server in the same way as Meteor.methods do for the other way.

Upvotes: 2

Related Questions